Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .changeset/default-children-retired-5051.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
'@object-ui/types': minor
'@object-ui/core': minor
'@object-ui/components': minor
---

The register-meta key `defaultChildren` is retired (objectui#5051).

It was declared in four places, produced in eleven, and read in **none**. The designer's
drop path builds a new node from its twin key only — `PageDesigner.tsx`,
`props: paletteItem?.defaultProps ?? {}` — with no `children:` line, so a palette item
that declared `defaultChildren` dropped an **empty** node and the declared children never
materialised. Nothing rendered the wrong thing; an entire declaration surface was simply
inert, which is the declared-but-unenforced shape ADR-0049 targets. Per the maintainer
ruling of 2026-08-19, the key is removed rather than wired up; if designer
default-children UX is ever product-wanted it returns as its own designed card.

**If you author plugins against the published register-meta table, drop the key.** It is
gone from `skills/objectui/guides/plugin-development.md`, which had been teaching it. A
meta that still declares it stays *valid*: `ComponentMetaSchema` is a plain `z.object`,
and measured on zod 4.4.3 that STRIPS unknown keys rather than rejecting them — so the
key is silently dropped from the parse output instead of failing validation. TypeScript
authors get the loud signal instead: all three `ComponentMeta` declarations
(`@object-ui/types` `base.ts` and `plugin-scope.ts`, `@object-ui/core` `Registry.ts`) no
longer offer it, so re-declaring it is now a compile error.

**No runtime behaviour changes in either direction.** No code path read the key before
this change, and the eleven producers that set it (`sidebar.tsx` x10, `span.tsx`) were
feeding a reader that did not exist. Dropping a `span` or any of the ten sidebar types
into the designer produces exactly the node it produced yesterday.

Two suites keep it retired, one per package: `packages/types` pins the zod twin (the key
is absent from the parse output, with a surviving sibling asserted present through the
same parse as the control) plus the two TS twins with `@ts-expect-error`, and
`packages/core` pins the registration surface the eleven producers were written against.
Both are compile-time-enforced through each package's chained `tsconfig.test.json`.
5 changes: 1 addition & 4 deletions packages/components/src/renderers/basic/span.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,6 @@ ComponentRegistry.register('span',
],
defaultProps: {
className: 'px-1.5 py-0.5 sm:px-2 sm:py-1'
},
defaultChildren: [
{ type: 'text', content: 'Inline text' }
]
}
}
);
54 changes: 10 additions & 44 deletions packages/components/src/renderers/navigation/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,7 @@ ComponentRegistry.register('sidebar-provider',
],
defaultProps: {
defaultOpen: true
},
defaultChildren: [
{ type: 'sidebar' },
{ type: 'sidebar-inset' }
]
}
}
);

Expand All @@ -73,12 +69,7 @@ ComponentRegistry.register('sidebar',
collapsible: 'icon',
side: 'left',
variant: 'sidebar'
},
defaultChildren: [
{ type: 'sidebar-header' },
{ type: 'sidebar-content' },
{ type: 'sidebar-footer' }
]
}
}
);

Expand All @@ -88,10 +79,7 @@ ComponentRegistry.register('sidebar-header',
),
{
namespace: 'ui',
label: 'Sidebar Header',
defaultChildren: [
{ type: 'text', content: 'Sidebar Header' }
]
label: 'Sidebar Header'
}
);

Expand All @@ -101,10 +89,7 @@ ComponentRegistry.register('sidebar-content',
),
{
namespace: 'ui',
label: 'Sidebar Content',
defaultChildren: [
{ type: 'sidebar-group' }
]
label: 'Sidebar Content'
}
);

Expand Down Expand Up @@ -136,10 +121,7 @@ ComponentRegistry.register('sidebar-group',
],
defaultProps: {
label: 'Menu'
},
defaultChildren: [
{ type: 'sidebar-menu' }
]
}
}
);

Expand All @@ -149,11 +131,7 @@ ComponentRegistry.register('sidebar-menu',
),
{
namespace: 'ui',
label: 'Sidebar Menu',
defaultChildren: [
{ type: 'sidebar-menu-item' },
{ type: 'sidebar-menu-item' }
]
label: 'Sidebar Menu'
}
);

Expand All @@ -163,10 +141,7 @@ ComponentRegistry.register('sidebar-menu-item',
),
{
namespace: 'ui',
label: 'Sidebar Menu Item',
defaultChildren: [
{ type: 'sidebar-menu-button' }
]
label: 'Sidebar Menu Item'
}
);

Expand All @@ -186,10 +161,7 @@ ComponentRegistry.register('sidebar-menu-button',
],
defaultProps: {
size: 'default'
},
defaultChildren: [
{ type: 'text', content: 'Menu Item' }
]
}
}
);

Expand All @@ -199,10 +171,7 @@ ComponentRegistry.register('sidebar-footer',
),
{
namespace: 'ui',
label: 'Sidebar Footer',
defaultChildren: [
{ type: 'text', content: 'Footer' }
]
label: 'Sidebar Footer'
}
);

Expand All @@ -212,10 +181,7 @@ ComponentRegistry.register('sidebar-inset',
),
{
namespace: 'ui',
label: 'Sidebar Inset',
defaultChildren: [
{ type: 'div', className: 'p-4', body: [{ type: 'text', content: 'Main content area' }] }
]
label: 'Sidebar Inset'
}
);

Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/registry/Registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*/

import type { ComponentInputControlType } from '@object-ui/types';
import type { SchemaNode } from '../types/index.js';
import { PUBLIC_BLOCKS } from './public-blocks.js';

export type ComponentRenderer<T = any> = T;
Expand Down Expand Up @@ -103,7 +102,6 @@ export type ComponentMeta = {
labelling?: 'control' | 'group' | 'display';
inputs?: ComponentInput[];
defaultProps?: Record<string, any>; // Default props when dropped
defaultChildren?: SchemaNode[]; // Default children when dropped
examples?: Record<string, any>; // Example configurations
isContainer?: boolean; // Whether the component can have children
resizable?: boolean; // Whether the component can be resized in the designer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* Retirement pin — the fourth declaration twin of the register-meta key
* `defaultChildren` (objectui#5051, ADR-0049 enforce-or-remove; maintainer
* ruling of 2026-08-19 adopted option B, "retire the key everywhere").
*
* `ComponentMeta` here is the registration surface every `ComponentRegistry.register`
* call is checked against, so it is the twin a producer would re-grow the key
* through: the eleven producers retired alongside it (`sidebar.tsx` x10,
* `span.tsx`) were all written against THIS type. Its three siblings — the two
* `ComponentMeta` interfaces and the `ComponentMetaSchema` validator in
* `@object-ui/types` — are pinned in that package.
*
* This is a COMPILE-TIME pin only, and deliberately claims nothing at runtime.
* The registry does not validate meta: it stores what a caller hands it, so a
* caller casting through `any` could still park the key on a config object at
* runtime and no assertion here could see it. `@ts-expect-error` is real
* enforcement because `packages/core/tsconfig.test.json` is chained from this
* package's `type-check` script (objectui#3009), which is what CI's Type Check
* job runs.
*/

import { describe, it, expect } from 'vitest';
import type { ComponentMeta } from '../Registry';

describe('ComponentMeta (core registry) — the retired key is gone from the registration surface', () => {
it('rejects `defaultChildren` at compile time', () => {
const retired: ComponentMeta = {
label: 'Inline Container',
// @ts-expect-error `defaultChildren` was retired by objectui#5051 — the
// designer's drop path reads `defaultProps` only, so the declared default
// children never materialised. Re-declaring it here re-opens the
// declared-but-unenforced gap ADR-0049 targets.
defaultChildren: [{ type: 'text', content: 'Inline text' }],
};
// Referenced so the binding is not merely unused — the `@ts-expect-error`
// above is the actual assertion.
expect(retired.label).toBe('Inline Container');
});

it('still offers the twin the designer actually reads', () => {
// Positive control: the surface is not simply refusing everything. This is
// the key `PageDesigner` consumes on drop, and it is untouched by #5051.
const legal: ComponentMeta = {
label: 'Inline Container',
defaultProps: { className: 'px-1.5 py-0.5' },
isContainer: true,
};
expect(legal.defaultProps).toEqual({ className: 'px-1.5 py-0.5' });
expect(legal.isContainer).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* Retirement pin — the register-meta key `defaultChildren` (objectui#5051,
* ADR-0049 enforce-or-remove; maintainer ruling of 2026-08-19 adopted option B,
* "retire the key everywhere").
*
* The key was declared in four places, produced in eleven, and read in NONE.
* The designer drag-and-drop path builds a dropped node from its twin key only
* (`PageDesigner.tsx`, `props: paletteItem?.defaultProps ?? {}`), with no
* `children:` line — so a palette item that declared `defaultChildren` dropped
* an empty node and the declaration never materialised. This package holds two
* of the four declaration twins plus the runtime one:
*
* 1. `ComponentMetaSchema` (`zod/base.zod.ts`) — the runtime validator.
* 2. `ComponentMeta` (`base.ts`) — the published `.d.ts` autocomplete surface,
* i.e. what a plugin author (or an AI author) copies from.
* 3. `ComponentMeta` (`plugin-scope.ts`) — its plugin-facing twin, which spelt
* the same key `any[]`.
*
* The Registry twin (`@object-ui/core`) is pinned in that package instead, so
* this suite does not have to import its own dependent.
*
* Two kinds of assertion, deliberately different because the surfaces differ:
*
* The Zod half is NOT a refusal. Measured on zod 4.4.3, a `z.object` STRIPS
* unknown keys rather than rejecting them, and `ComponentMetaSchema` is a plain
* `z.object` with no `.strict()`. So the honest pin is that the key is silently
* DROPPED from the parse output — which is precisely the behaviour the ruling's
* confidence-gap note called out for external authors who already declare it.
* Asserting a rejection here would pin a verdict this validator never emits.
*
* That "absent from the output" assertion is worthless on its own — a schema
* that stripped EVERYTHING would satisfy it — so each case carries its own
* positive control: a surviving sibling key asserted PRESENT in the same parse
* output, through the same call.
*
* The two TS halves erase at runtime, so they are pinned with
* `@ts-expect-error`, which is real enforcement only because
* `packages/types/tsconfig.test.json` is chained from this package's
* `type-check` script (#3009).
*/

import { describe, it, expect } from 'vitest';
import { ComponentMetaSchema } from '../zod/base.zod.js';
import type { ComponentMeta } from '../base.js';
import type { ComponentMeta as PluginScopeComponentMeta } from '../plugin-scope.js';

/** The retired key, and the twin the designer actually reads. */
const RETIRED = 'defaultChildren';
const SURVIVOR = 'defaultProps';

describe('ComponentMetaSchema — the runtime twin no longer carries the retired key', () => {
it('drops an authored `defaultChildren` from the parse output, while carrying its surviving twin through the same parse', () => {
const result = ComponentMetaSchema.safeParse({
label: 'Inline Container',
defaultProps: { className: 'px-1.5' },
defaultChildren: [{ type: 'text', content: 'Inline text' }],
});

// Not a refusal: the schema strips, so an author who still declares the key
// keeps a VALID meta. The retirement is that the key stops travelling.
expect(result.success).toBe(true);
if (!result.success) return;

expect(result.data).not.toHaveProperty(RETIRED);

// Positive control, same parse, same call: the schema is still capable of
// carrying a key through. Without this, a schema that dropped every key
// would pass the assertion above.
expect(result.data).toHaveProperty(SURVIVOR);
expect(result.data.defaultProps).toEqual({ className: 'px-1.5' });
});

it('shrank by exactly one member — the sibling meta vocabulary still round-trips', () => {
// Guards the removal from over-reaching: a hand-edited schema that dropped
// a neighbour would otherwise pass every assertion above.
const result = ComponentMetaSchema.safeParse({
label: 'Sidebar',
icon: 'panel-left',
category: 'Navigation',
defaultProps: { collapsible: 'icon' },
examples: { basic: {} },
isContainer: true,
resizable: true,
tags: ['navigation'],
description: 'A sidebar',
});

expect(result.success).toBe(true);
if (!result.success) return;

for (const key of [
'label',
'icon',
'category',
'defaultProps',
'examples',
'isContainer',
'resizable',
'tags',
'description',
]) {
expect(result.data).toHaveProperty(key);
}
});
});

describe('the published TS twins no longer offer the retired key', () => {
it('`ComponentMeta` (base.ts) rejects it at compile time', () => {
const legal: ComponentMeta = { label: 'Span', defaultProps: { className: 'px-1' } };
expect(legal.defaultProps).toEqual({ className: 'px-1' });

const retired: ComponentMeta = {
label: 'Span',
// @ts-expect-error `defaultChildren` was retired by objectui#5051 — it had
// no consumer, so the declared default children never materialised on drop.
defaultChildren: [{ type: 'text', content: 'Inline text' }],
};
// Referenced so the binding is not merely unused — the `@ts-expect-error`
// above is the actual assertion, enforced by `tsconfig.test.json`.
expect(retired.label).toBe('Span');
});

it('`ComponentMeta` (plugin-scope.ts) rejects it at compile time', () => {
const legal: PluginScopeComponentMeta = { label: 'Span', defaultProps: { className: 'px-1' } };
expect(legal.defaultProps).toEqual({ className: 'px-1' });

const retired: PluginScopeComponentMeta = {
label: 'Span',
// @ts-expect-error `defaultChildren` was retired by objectui#5051; the
// plugin-facing twin spelt it `any[]` and is retired in lockstep.
defaultChildren: [{ type: 'text', content: 'Inline text' }],
};
expect(retired.label).toBe('Span');
});
});
Loading
Loading