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
32 changes: 32 additions & 0 deletions .changeset/6298-componentconfig-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
---
'@object-ui/types': minor
'@object-ui/core': minor
---

`ComponentConfig` now has one authority: `@object-ui/types` declares it, `@object-ui/core` re-exports it

`@object-ui/types` and `@object-ui/core` each published a declaration of
`ComponentConfig`, so an auto-import picked between two different types by
alphabetical order. After the `ComponentMeta` convergence the remaining
difference was genericity and the `component` slot: `@object-ui/types`' was
non-generic with `component: any`, core's was `<T = any>` with
`component: ComponentRenderer<T>`.

`@object-ui/types`' declaration gains that type parameter, **defaulted**, so
every existing spelling keeps its meaning exactly — bare `ComponentConfig` is
`ComponentConfig<any>`, whose `component` is `any`, as before. `@object-ui/core`
re-exports it instead of declaring its own.

The registry-only keys (`tier`, `namespace`, `skipFallback`, `labelling`,
`deprecated`) were not dropped: they moved to a named extension,
`RegistryComponentConfig`, which is what `Registry.getConfig`,
`getAllConfigs` and `getNamespaceComponents` return. Those return values are
type-identical to what they returned before, so every read path is unchanged.

**Breaking:** a consumer that imports `ComponentConfig` from `@object-ui/core`
*and* touches one of those five registry-only keys through that annotation must
switch the annotation to `RegistryComponentConfig` — the name `ComponentConfig`
no longer carries them there. Filed `minor` rather than `major` per AGENTS.md's
versioning policy: objectui's own breaking changes ship as `minor` with the break
spelled out here, because the whole publishable set is one changeset `fixed` group
pinned to `@objectstack`'s major.
2 changes: 1 addition & 1 deletion content/docs/guide/plugin-development.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,7 +348,7 @@ import { ComponentRegistry } from '@object-ui/core';

ComponentRegistry.has('board'); // boolean
ComponentRegistry.getAllTypes(); // string[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // ComponentConfig[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // RegistryComponentConfig[]
```

## Plugin Configuration & Schema Types
Expand Down
99 changes: 90 additions & 9 deletions packages/core/src/registry/Registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,12 +7,38 @@
*/

import type { ComponentMeta as CanonicalComponentMeta } from '@object-ui/types';
// Deliberately a SECOND import statement from the same module rather than a
// widening of the line above: `__tests__/component-meta-derives-from-canonical.test.ts`
// pins that line as an exact string (objectui#6067's source-identity assertion),
// so adding a specifier to it would red a pin that has nothing to do with this
// name. Two `import type` lines from one module is legal and costs nothing —
// type imports are erased.
import type { ComponentConfig } from '@object-ui/types';
import {
ELEMENT_DATA_SOURCE_INPUT,
isElementDataSourceBlock,
} from '../data-scope/element-data-source.js';
import { PUBLIC_BLOCKS } from './public-blocks.js';

/**
* The renderer a registration carries — the IDENTITY alias, deliberately.
*
* ⚠️ Load-bearing for objectui#6298, which is why it is documented rather than
* left as a bare line. `ComponentRenderer<T>` resolves to `T` and contributes
* NO type information: that is the entire reason `@object-ui/types` can declare
* the one `ComponentConfig` with `component: T` and mean exactly what this
* package used to mean by `component: ComponentRenderer<T>`, WITHOUT
* `@object-ui/types` needing to reach this declaration. It could not have
* reached it: `@object-ui/types` is the bottom layer (`packages/types/package.json`
* depends on `@objectstack/spec` and `zod` only) and this package depends on it,
* so an edge in that direction would be a cycle.
*
* The alias survives as the NAME this package's own renderer-shaped positions
* are spelled with ({@link withElementDataSourceInput}, {@link PublicComponentConfig}).
* If it is ever given real content, `component: T` over in `@object-ui/types`
* stops being the same slot — `__tests__/component-config-single-declaration.test.ts`
* asserts the identity so that change cannot pass unnoticed.
*/
export type ComponentRenderer<T = any> = T;

/**
Expand DownExpand Up@@ -233,16 +259,71 @@ export type RegistryComponentMetaExtras = {
*/
export type ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras;

export type ComponentConfig<T = any> = ComponentMeta & {
type: string;
component: ComponentRenderer<T>;
};
/**
* ONE authority for `ComponentConfig` (objectui#6298) — this package RE-EXPORTS
* `@object-ui/types`' declaration instead of declaring a second one, the same
* disposition objectui#5671 gave `ComponentInput` a few lines above and
* objectui#4580 ruled for the whole family: *a structural copy would reproduce
* the defect the moment either side moved.*
*
* ## What was wrong
*
* Both spellings were PUBLISHED — `@object-ui/types`' `src/index.ts` exports its
* one, and this file reaches `@object-ui/core`'s public entry through
* `src/index.ts`'s `export * from './registry/Registry.js'`. An IDE auto-import
* therefore picked between two different types by alphabetical order. After
* objectui#6067 / PR #6297 single-sourced the `ComponentMeta` half, what still
* differed was GENERICITY AND THE `component` SLOT: `@object-ui/types`' was
* non-generic with `component: any`, this one was `<T = any>` with
* `component: ComponentRenderer<T>`.
*
* ⚠️ Measured on the EMITTED `.d.ts` of both packages immediately before this
* convergence, `Exact<TypesConfig, CoreConfig>` — mutual assignability — read
* `true` on the DIVERGED pair, because `component: any` absorbs everything and
* every other member is optional. An assignability assertion is a GHOST here,
* exactly as `__tests__/component-meta-derives-from-canonical.test.ts` records
* for the sibling type. The readings that actually moved were "is
* `@object-ui/types`' declaration generic" (`TS2315: Type 'ComponentConfig' is
* not generic` before, no error after) and the symmetric key-set difference.
*
* A re-export is not a second authority — `scripts/__tests__/one-authority-per-exported-name-6273.test.ts`
* counts declarations and ALIASING re-exports, never `export type { X } from …`
* — which is why this convergence takes `ComponentConfig` off that gate's
* `KNOWN_COLLISIONS` baseline. Deriving a new declaration here instead would
* NOT have: `ComponentMeta` was converged that way by PR #6297 and is still a
* row on that baseline today.
*/
export type { ComponentConfig } from '@object-ui/types';

/**
* What the registry actually STORES and hands back — `ComponentConfig` plus the
* registry-only keys, as a NAMED extension (objectui#6298).
*
* This is type-identical to the `ComponentConfig` this file used to declare:
* `CanonicalComponentMeta & RegistryComponentMetaExtras & { type; component }`,
* reached from the other side. The two halves are named rather than restated —
* one declaration for the shared members ({@link ComponentConfig}, in
* `@object-ui/types`), a named extension for the rest
* ({@link RegistryComponentMetaExtras}) — which is the shape PR #6297 gave
* {@link ComponentMeta}.
*
* It exists because the extras are NOT optional decoration on a registry entry:
* {@link Registry.getNamespaceComponents} filters on `config.namespace`, and
* `tier` / `labelling` / `deprecated` are read off registrations elsewhere. A
* bare re-export as the entry type would have silently dropped them.
*
* ⚠️ `ComponentConfig` remains the AUTHORING vocabulary and the general name;
* registrations are checked against {@link ComponentMeta}, never against this.
* Nothing writes a `RegistryComponentConfig` literal — the registry builds them.
*/
export type RegistryComponentConfig<T = any> = ComponentConfig<T> &
RegistryComponentMetaExtras;

/**
* A CONTRACT-surface entry (ADR-0080), as returned by
* {@link Registry.getPublicConfigs}.
*
* Same shape as {@link ComponentConfig} except `component` is absent while the
* Same shape as {@link RegistryComponentConfig} except `component` is absent while the
* entry is still a pending `registerLazy` stub: the plugin module has not been
* imported yet, so there is no renderer to hand out. Consumers render such an
* entry through `SchemaRenderer`, which triggers the loader and shows a
Expand DownExpand Up@@ -314,7 +395,7 @@ export function withElementDataSourceInput<T>(
}

export class Registry<T = any> {
private components = new Map<string, ComponentConfig<T>>();
private components = new Map<string, RegistryComponentConfig<T>>();
private lazyEntries = new Map<string, LazyEntry>();
/**
* Notifies subscribers that the registry has changed (new components
Expand DownExpand Up@@ -565,7 +646,7 @@ export class Registry<T = any> {
* @param namespace - Optional namespace for lookup priority
* @returns Component configuration or undefined
*/
getConfig(type: string, namespace?: string): ComponentConfig<T> | undefined {
getConfig(type: string, namespace?: string): RegistryComponentConfig<T> | undefined {
// If namespace is explicitly provided, ONLY look in that namespace (no fallback)
if (namespace) {
const namespacedType = `${namespace}:${type}`;
Expand DownExpand Up@@ -686,7 +767,7 @@ export class Registry<T = any> {
*
* @returns Array of all component configurations
*/
getAllConfigs(): ComponentConfig<T>[] {
getAllConfigs(): RegistryComponentConfig<T>[] {
return Array.from(this.components.values());
}

Expand DownExpand Up@@ -755,7 +836,7 @@ export class Registry<T = any> {
* @param namespace - Namespace to filter by
* @returns Array of component configurations in the namespace
*/
getNamespaceComponents(namespace: string): ComponentConfig<T>[] {
getNamespaceComponents(namespace: string): RegistryComponentConfig<T>[] {
return Array.from(this.components.values()).filter(
config => config.namespace === namespace
);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(types,core): one authority for `ComponentConfig` by claude[bot] · Pull Request #6937 · objectstack-ai/objectui · GitHub
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
32 changes: 32 additions & 0 deletions .changeset/6298-componentconfig-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
---
'@object-ui/types': minor
'@object-ui/core': minor
---

`ComponentConfig` now has one authority: `@object-ui/types` declares it, `@object-ui/core` re-exports it

`@object-ui/types` and `@object-ui/core` each published a declaration of
`ComponentConfig`, so an auto-import picked between two different types by
alphabetical order. After the `ComponentMeta` convergence the remaining
difference was genericity and the `component` slot: `@object-ui/types`' was
non-generic with `component: any`, core's was `<T = any>` with
`component: ComponentRenderer<T>`.

`@object-ui/types`' declaration gains that type parameter, **defaulted**, so
every existing spelling keeps its meaning exactly — bare `ComponentConfig` is
`ComponentConfig<any>`, whose `component` is `any`, as before. `@object-ui/core`
re-exports it instead of declaring its own.

The registry-only keys (`tier`, `namespace`, `skipFallback`, `labelling`,
`deprecated`) were not dropped: they moved to a named extension,
`RegistryComponentConfig`, which is what `Registry.getConfig`,
`getAllConfigs` and `getNamespaceComponents` return. Those return values are
type-identical to what they returned before, so every read path is unchanged.

**Breaking:** a consumer that imports `ComponentConfig` from `@object-ui/core`
*and* touches one of those five registry-only keys through that annotation must
switch the annotation to `RegistryComponentConfig` — the name `ComponentConfig`
no longer carries them there. Filed `minor` rather than `major` per AGENTS.md's
versioning policy: objectui's own breaking changes ship as `minor` with the break
spelled out here, because the whole publishable set is one changeset `fixed` group
pinned to `@objectstack`'s major.
2 changes: 1 addition & 1 deletion content/docs/guide/plugin-development.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,7 +348,7 @@ import { ComponentRegistry } from '@object-ui/core';

ComponentRegistry.has('board'); // boolean
ComponentRegistry.getAllTypes(); // string[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // ComponentConfig[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // RegistryComponentConfig[]
```

## Plugin Configuration & Schema Types
Expand Down
99 changes: 90 additions & 9 deletions packages/core/src/registry/Registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,12 +7,38 @@
*/

import type { ComponentMeta as CanonicalComponentMeta } from '@object-ui/types';
// Deliberately a SECOND import statement from the same module rather than a
// widening of the line above: `__tests__/component-meta-derives-from-canonical.test.ts`
// pins that line as an exact string (objectui#6067's source-identity assertion),
// so adding a specifier to it would red a pin that has nothing to do with this
// name. Two `import type` lines from one module is legal and costs nothing —
// type imports are erased.
import type { ComponentConfig } from '@object-ui/types';
import {
ELEMENT_DATA_SOURCE_INPUT,
isElementDataSourceBlock,
} from '../data-scope/element-data-source.js';
import { PUBLIC_BLOCKS } from './public-blocks.js';

/**
* The renderer a registration carries — the IDENTITY alias, deliberately.
*
* ⚠️ Load-bearing for objectui#6298, which is why it is documented rather than
* left as a bare line. `ComponentRenderer<T>` resolves to `T` and contributes
* NO type information: that is the entire reason `@object-ui/types` can declare
* the one `ComponentConfig` with `component: T` and mean exactly what this
* package used to mean by `component: ComponentRenderer<T>`, WITHOUT
* `@object-ui/types` needing to reach this declaration. It could not have
* reached it: `@object-ui/types` is the bottom layer (`packages/types/package.json`
* depends on `@objectstack/spec` and `zod` only) and this package depends on it,
* so an edge in that direction would be a cycle.
*
* The alias survives as the NAME this package's own renderer-shaped positions
* are spelled with ({@link withElementDataSourceInput}, {@link PublicComponentConfig}).
* If it is ever given real content, `component: T` over in `@object-ui/types`
* stops being the same slot — `__tests__/component-config-single-declaration.test.ts`
* asserts the identity so that change cannot pass unnoticed.
*/
export type ComponentRenderer<T = any> = T;

/**
Expand DownExpand Up@@ -233,16 +259,71 @@ export type RegistryComponentMetaExtras = {
*/
export type ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras;

export type ComponentConfig<T = any> = ComponentMeta & {
type: string;
component: ComponentRenderer<T>;
};
/**
* ONE authority for `ComponentConfig` (objectui#6298) — this package RE-EXPORTS
* `@object-ui/types`' declaration instead of declaring a second one, the same
* disposition objectui#5671 gave `ComponentInput` a few lines above and
* objectui#4580 ruled for the whole family: *a structural copy would reproduce
* the defect the moment either side moved.*
*
* ## What was wrong
*
* Both spellings were PUBLISHED — `@object-ui/types`' `src/index.ts` exports its
* one, and this file reaches `@object-ui/core`'s public entry through
* `src/index.ts`'s `export * from './registry/Registry.js'`. An IDE auto-import
* therefore picked between two different types by alphabetical order. After
* objectui#6067 / PR #6297 single-sourced the `ComponentMeta` half, what still
* differed was GENERICITY AND THE `component` SLOT: `@object-ui/types`' was
* non-generic with `component: any`, this one was `<T = any>` with
* `component: ComponentRenderer<T>`.
*
* ⚠️ Measured on the EMITTED `.d.ts` of both packages immediately before this
* convergence, `Exact<TypesConfig, CoreConfig>` — mutual assignability — read
* `true` on the DIVERGED pair, because `component: any` absorbs everything and
* every other member is optional. An assignability assertion is a GHOST here,
* exactly as `__tests__/component-meta-derives-from-canonical.test.ts` records
* for the sibling type. The readings that actually moved were "is
* `@object-ui/types`' declaration generic" (`TS2315: Type 'ComponentConfig' is
* not generic` before, no error after) and the symmetric key-set difference.
*
* A re-export is not a second authority — `scripts/__tests__/one-authority-per-exported-name-6273.test.ts`
* counts declarations and ALIASING re-exports, never `export type { X } from …`
* — which is why this convergence takes `ComponentConfig` off that gate's
* `KNOWN_COLLISIONS` baseline. Deriving a new declaration here instead would
* NOT have: `ComponentMeta` was converged that way by PR #6297 and is still a
* row on that baseline today.
*/
export type { ComponentConfig } from '@object-ui/types';

/**
* What the registry actually STORES and hands back — `ComponentConfig` plus the
* registry-only keys, as a NAMED extension (objectui#6298).
*
* This is type-identical to the `ComponentConfig` this file used to declare:
* `CanonicalComponentMeta & RegistryComponentMetaExtras & { type; component }`,
* reached from the other side. The two halves are named rather than restated —
* one declaration for the shared members ({@link ComponentConfig}, in
* `@object-ui/types`), a named extension for the rest
* ({@link RegistryComponentMetaExtras}) — which is the shape PR #6297 gave
* {@link ComponentMeta}.
*
* It exists because the extras are NOT optional decoration on a registry entry:
* {@link Registry.getNamespaceComponents} filters on `config.namespace`, and
* `tier` / `labelling` / `deprecated` are read off registrations elsewhere. A
* bare re-export as the entry type would have silently dropped them.
*
* ⚠️ `ComponentConfig` remains the AUTHORING vocabulary and the general name;
* registrations are checked against {@link ComponentMeta}, never against this.
* Nothing writes a `RegistryComponentConfig` literal — the registry builds them.
*/
export type RegistryComponentConfig<T = any> = ComponentConfig<T> &
RegistryComponentMetaExtras;

/**
* A CONTRACT-surface entry (ADR-0080), as returned by
* {@link Registry.getPublicConfigs}.
*
* Same shape as {@link ComponentConfig} except `component` is absent while the
* Same shape as {@link RegistryComponentConfig} except `component` is absent while the
* entry is still a pending `registerLazy` stub: the plugin module has not been
* imported yet, so there is no renderer to hand out. Consumers render such an
* entry through `SchemaRenderer`, which triggers the loader and shows a
Expand DownExpand Up@@ -314,7 +395,7 @@ export function withElementDataSourceInput<T>(
}

export class Registry<T = any> {
private components = new Map<string, ComponentConfig<T>>();
private components = new Map<string, RegistryComponentConfig<T>>();
private lazyEntries = new Map<string, LazyEntry>();
/**
* Notifies subscribers that the registry has changed (new components
Expand DownExpand Up@@ -565,7 +646,7 @@ export class Registry<T = any> {
* @param namespace - Optional namespace for lookup priority
* @returns Component configuration or undefined
*/
getConfig(type: string, namespace?: string): ComponentConfig<T> | undefined {
getConfig(type: string, namespace?: string): RegistryComponentConfig<T> | undefined {
// If namespace is explicitly provided, ONLY look in that namespace (no fallback)
if (namespace) {
const namespacedType = `${namespace}:${type}`;
Expand DownExpand Up@@ -686,7 +767,7 @@ export class Registry<T = any> {
*
* @returns Array of all component configurations
*/
getAllConfigs(): ComponentConfig<T>[] {
getAllConfigs(): RegistryComponentConfig<T>[] {
return Array.from(this.components.values());
}

Expand DownExpand Up@@ -755,7 +836,7 @@ export class Registry<T = any> {
* @param namespace - Namespace to filter by
* @returns Array of component configurations in the namespace
*/
getNamespaceComponents(namespace: string): ComponentConfig<T>[] {
getNamespaceComponents(namespace: string): RegistryComponentConfig<T>[] {
return Array.from(this.components.values()).filter(
config => config.namespace === namespace
);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(types,core): one authority for `ComponentConfig` by claude[bot] · Pull Request #6937 · objectstack-ai/objectui · GitHub
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
32 changes: 32 additions & 0 deletions .changeset/6298-componentconfig-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
---
'@object-ui/types': minor
'@object-ui/core': minor
---

`ComponentConfig` now has one authority: `@object-ui/types` declares it, `@object-ui/core` re-exports it

`@object-ui/types` and `@object-ui/core` each published a declaration of
`ComponentConfig`, so an auto-import picked between two different types by
alphabetical order. After the `ComponentMeta` convergence the remaining
difference was genericity and the `component` slot: `@object-ui/types`' was
non-generic with `component: any`, core's was `<T = any>` with
`component: ComponentRenderer<T>`.

`@object-ui/types`' declaration gains that type parameter, **defaulted**, so
every existing spelling keeps its meaning exactly — bare `ComponentConfig` is
`ComponentConfig<any>`, whose `component` is `any`, as before. `@object-ui/core`
re-exports it instead of declaring its own.

The registry-only keys (`tier`, `namespace`, `skipFallback`, `labelling`,
`deprecated`) were not dropped: they moved to a named extension,
`RegistryComponentConfig`, which is what `Registry.getConfig`,
`getAllConfigs` and `getNamespaceComponents` return. Those return values are
type-identical to what they returned before, so every read path is unchanged.

**Breaking:** a consumer that imports `ComponentConfig` from `@object-ui/core`
*and* touches one of those five registry-only keys through that annotation must
switch the annotation to `RegistryComponentConfig` — the name `ComponentConfig`
no longer carries them there. Filed `minor` rather than `major` per AGENTS.md's
versioning policy: objectui's own breaking changes ship as `minor` with the break
spelled out here, because the whole publishable set is one changeset `fixed` group
pinned to `@objectstack`'s major.
2 changes: 1 addition & 1 deletion content/docs/guide/plugin-development.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,7 +348,7 @@ import { ComponentRegistry } from '@object-ui/core';

ComponentRegistry.has('board'); // boolean
ComponentRegistry.getAllTypes(); // string[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // ComponentConfig[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // RegistryComponentConfig[]
```

## Plugin Configuration & Schema Types
Expand Down
99 changes: 90 additions & 9 deletions packages/core/src/registry/Registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,12 +7,38 @@
*/

import type { ComponentMeta as CanonicalComponentMeta } from '@object-ui/types';
// Deliberately a SECOND import statement from the same module rather than a
// widening of the line above: `__tests__/component-meta-derives-from-canonical.test.ts`
// pins that line as an exact string (objectui#6067's source-identity assertion),
// so adding a specifier to it would red a pin that has nothing to do with this
// name. Two `import type` lines from one module is legal and costs nothing —
// type imports are erased.
import type { ComponentConfig } from '@object-ui/types';
import {
ELEMENT_DATA_SOURCE_INPUT,
isElementDataSourceBlock,
} from '../data-scope/element-data-source.js';
import { PUBLIC_BLOCKS } from './public-blocks.js';

/**
* The renderer a registration carries — the IDENTITY alias, deliberately.
*
* ⚠️ Load-bearing for objectui#6298, which is why it is documented rather than
* left as a bare line. `ComponentRenderer<T>` resolves to `T` and contributes
* NO type information: that is the entire reason `@object-ui/types` can declare
* the one `ComponentConfig` with `component: T` and mean exactly what this
* package used to mean by `component: ComponentRenderer<T>`, WITHOUT
* `@object-ui/types` needing to reach this declaration. It could not have
* reached it: `@object-ui/types` is the bottom layer (`packages/types/package.json`
* depends on `@objectstack/spec` and `zod` only) and this package depends on it,
* so an edge in that direction would be a cycle.
*
* The alias survives as the NAME this package's own renderer-shaped positions
* are spelled with ({@link withElementDataSourceInput}, {@link PublicComponentConfig}).
* If it is ever given real content, `component: T` over in `@object-ui/types`
* stops being the same slot — `__tests__/component-config-single-declaration.test.ts`
* asserts the identity so that change cannot pass unnoticed.
*/
export type ComponentRenderer<T = any> = T;

/**
Expand DownExpand Up@@ -233,16 +259,71 @@ export type RegistryComponentMetaExtras = {
*/
export type ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras;

export type ComponentConfig<T = any> = ComponentMeta & {
type: string;
component: ComponentRenderer<T>;
};
/**
* ONE authority for `ComponentConfig` (objectui#6298) — this package RE-EXPORTS
* `@object-ui/types`' declaration instead of declaring a second one, the same
* disposition objectui#5671 gave `ComponentInput` a few lines above and
* objectui#4580 ruled for the whole family: *a structural copy would reproduce
* the defect the moment either side moved.*
*
* ## What was wrong
*
* Both spellings were PUBLISHED — `@object-ui/types`' `src/index.ts` exports its
* one, and this file reaches `@object-ui/core`'s public entry through
* `src/index.ts`'s `export * from './registry/Registry.js'`. An IDE auto-import
* therefore picked between two different types by alphabetical order. After
* objectui#6067 / PR #6297 single-sourced the `ComponentMeta` half, what still
* differed was GENERICITY AND THE `component` SLOT: `@object-ui/types`' was
* non-generic with `component: any`, this one was `<T = any>` with
* `component: ComponentRenderer<T>`.
*
* ⚠️ Measured on the EMITTED `.d.ts` of both packages immediately before this
* convergence, `Exact<TypesConfig, CoreConfig>` — mutual assignability — read
* `true` on the DIVERGED pair, because `component: any` absorbs everything and
* every other member is optional. An assignability assertion is a GHOST here,
* exactly as `__tests__/component-meta-derives-from-canonical.test.ts` records
* for the sibling type. The readings that actually moved were "is
* `@object-ui/types`' declaration generic" (`TS2315: Type 'ComponentConfig' is
* not generic` before, no error after) and the symmetric key-set difference.
*
* A re-export is not a second authority — `scripts/__tests__/one-authority-per-exported-name-6273.test.ts`
* counts declarations and ALIASING re-exports, never `export type { X } from …`
* — which is why this convergence takes `ComponentConfig` off that gate's
* `KNOWN_COLLISIONS` baseline. Deriving a new declaration here instead would
* NOT have: `ComponentMeta` was converged that way by PR #6297 and is still a
* row on that baseline today.
*/
export type { ComponentConfig } from '@object-ui/types';

/**
* What the registry actually STORES and hands back — `ComponentConfig` plus the
* registry-only keys, as a NAMED extension (objectui#6298).
*
* This is type-identical to the `ComponentConfig` this file used to declare:
* `CanonicalComponentMeta & RegistryComponentMetaExtras & { type; component }`,
* reached from the other side. The two halves are named rather than restated —
* one declaration for the shared members ({@link ComponentConfig}, in
* `@object-ui/types`), a named extension for the rest
* ({@link RegistryComponentMetaExtras}) — which is the shape PR #6297 gave
* {@link ComponentMeta}.
*
* It exists because the extras are NOT optional decoration on a registry entry:
* {@link Registry.getNamespaceComponents} filters on `config.namespace`, and
* `tier` / `labelling` / `deprecated` are read off registrations elsewhere. A
* bare re-export as the entry type would have silently dropped them.
*
* ⚠️ `ComponentConfig` remains the AUTHORING vocabulary and the general name;
* registrations are checked against {@link ComponentMeta}, never against this.
* Nothing writes a `RegistryComponentConfig` literal — the registry builds them.
*/
export type RegistryComponentConfig<T = any> = ComponentConfig<T> &
RegistryComponentMetaExtras;

/**
* A CONTRACT-surface entry (ADR-0080), as returned by
* {@link Registry.getPublicConfigs}.
*
* Same shape as {@link ComponentConfig} except `component` is absent while the
* Same shape as {@link RegistryComponentConfig} except `component` is absent while the
* entry is still a pending `registerLazy` stub: the plugin module has not been
* imported yet, so there is no renderer to hand out. Consumers render such an
* entry through `SchemaRenderer`, which triggers the loader and shows a
Expand DownExpand Up@@ -314,7 +395,7 @@ export function withElementDataSourceInput<T>(
}

export class Registry<T = any> {
private components = new Map<string, ComponentConfig<T>>();
private components = new Map<string, RegistryComponentConfig<T>>();
private lazyEntries = new Map<string, LazyEntry>();
/**
* Notifies subscribers that the registry has changed (new components
Expand DownExpand Up@@ -565,7 +646,7 @@ export class Registry<T = any> {
* @param namespace - Optional namespace for lookup priority
* @returns Component configuration or undefined
*/
getConfig(type: string, namespace?: string): ComponentConfig<T> | undefined {
getConfig(type: string, namespace?: string): RegistryComponentConfig<T> | undefined {
// If namespace is explicitly provided, ONLY look in that namespace (no fallback)
if (namespace) {
const namespacedType = `${namespace}:${type}`;
Expand DownExpand Up@@ -686,7 +767,7 @@ export class Registry<T = any> {
*
* @returns Array of all component configurations
*/
getAllConfigs(): ComponentConfig<T>[] {
getAllConfigs(): RegistryComponentConfig<T>[] {
return Array.from(this.components.values());
}

Expand DownExpand Up@@ -755,7 +836,7 @@ export class Registry<T = any> {
* @param namespace - Namespace to filter by
* @returns Array of component configurations in the namespace
*/
getNamespaceComponents(namespace: string): ComponentConfig<T>[] {
getNamespaceComponents(namespace: string): RegistryComponentConfig<T>[] {
return Array.from(this.components.values()).filter(
config => config.namespace === namespace
);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(types,core): one authority for `ComponentConfig` by claude[bot] · Pull Request #6937 · objectstack-ai/objectui · GitHub
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
32 changes: 32 additions & 0 deletions .changeset/6298-componentconfig-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
---
'@object-ui/types': minor
'@object-ui/core': minor
---

`ComponentConfig` now has one authority: `@object-ui/types` declares it, `@object-ui/core` re-exports it

`@object-ui/types` and `@object-ui/core` each published a declaration of
`ComponentConfig`, so an auto-import picked between two different types by
alphabetical order. After the `ComponentMeta` convergence the remaining
difference was genericity and the `component` slot: `@object-ui/types`' was
non-generic with `component: any`, core's was `<T = any>` with
`component: ComponentRenderer<T>`.

`@object-ui/types`' declaration gains that type parameter, **defaulted**, so
every existing spelling keeps its meaning exactly — bare `ComponentConfig` is
`ComponentConfig<any>`, whose `component` is `any`, as before. `@object-ui/core`
re-exports it instead of declaring its own.

The registry-only keys (`tier`, `namespace`, `skipFallback`, `labelling`,
`deprecated`) were not dropped: they moved to a named extension,
`RegistryComponentConfig`, which is what `Registry.getConfig`,
`getAllConfigs` and `getNamespaceComponents` return. Those return values are
type-identical to what they returned before, so every read path is unchanged.

**Breaking:** a consumer that imports `ComponentConfig` from `@object-ui/core`
*and* touches one of those five registry-only keys through that annotation must
switch the annotation to `RegistryComponentConfig` — the name `ComponentConfig`
no longer carries them there. Filed `minor` rather than `major` per AGENTS.md's
versioning policy: objectui's own breaking changes ship as `minor` with the break
spelled out here, because the whole publishable set is one changeset `fixed` group
pinned to `@objectstack`'s major.
2 changes: 1 addition & 1 deletion content/docs/guide/plugin-development.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,7 +348,7 @@ import { ComponentRegistry } from '@object-ui/core';

ComponentRegistry.has('board'); // boolean
ComponentRegistry.getAllTypes(); // string[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // ComponentConfig[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // RegistryComponentConfig[]
```

## Plugin Configuration & Schema Types
Expand Down
99 changes: 90 additions & 9 deletions packages/core/src/registry/Registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,12 +7,38 @@
*/

import type { ComponentMeta as CanonicalComponentMeta } from '@object-ui/types';
// Deliberately a SECOND import statement from the same module rather than a
// widening of the line above: `__tests__/component-meta-derives-from-canonical.test.ts`
// pins that line as an exact string (objectui#6067's source-identity assertion),
// so adding a specifier to it would red a pin that has nothing to do with this
// name. Two `import type` lines from one module is legal and costs nothing —
// type imports are erased.
import type { ComponentConfig } from '@object-ui/types';
import {
ELEMENT_DATA_SOURCE_INPUT,
isElementDataSourceBlock,
} from '../data-scope/element-data-source.js';
import { PUBLIC_BLOCKS } from './public-blocks.js';

/**
* The renderer a registration carries — the IDENTITY alias, deliberately.
*
* ⚠️ Load-bearing for objectui#6298, which is why it is documented rather than
* left as a bare line. `ComponentRenderer<T>` resolves to `T` and contributes
* NO type information: that is the entire reason `@object-ui/types` can declare
* the one `ComponentConfig` with `component: T` and mean exactly what this
* package used to mean by `component: ComponentRenderer<T>`, WITHOUT
* `@object-ui/types` needing to reach this declaration. It could not have
* reached it: `@object-ui/types` is the bottom layer (`packages/types/package.json`
* depends on `@objectstack/spec` and `zod` only) and this package depends on it,
* so an edge in that direction would be a cycle.
*
* The alias survives as the NAME this package's own renderer-shaped positions
* are spelled with ({@link withElementDataSourceInput}, {@link PublicComponentConfig}).
* If it is ever given real content, `component: T` over in `@object-ui/types`
* stops being the same slot — `__tests__/component-config-single-declaration.test.ts`
* asserts the identity so that change cannot pass unnoticed.
*/
export type ComponentRenderer<T = any> = T;

/**
Expand DownExpand Up@@ -233,16 +259,71 @@ export type RegistryComponentMetaExtras = {
*/
export type ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras;

export type ComponentConfig<T = any> = ComponentMeta & {
type: string;
component: ComponentRenderer<T>;
};
/**
* ONE authority for `ComponentConfig` (objectui#6298) — this package RE-EXPORTS
* `@object-ui/types`' declaration instead of declaring a second one, the same
* disposition objectui#5671 gave `ComponentInput` a few lines above and
* objectui#4580 ruled for the whole family: *a structural copy would reproduce
* the defect the moment either side moved.*
*
* ## What was wrong
*
* Both spellings were PUBLISHED — `@object-ui/types`' `src/index.ts` exports its
* one, and this file reaches `@object-ui/core`'s public entry through
* `src/index.ts`'s `export * from './registry/Registry.js'`. An IDE auto-import
* therefore picked between two different types by alphabetical order. After
* objectui#6067 / PR #6297 single-sourced the `ComponentMeta` half, what still
* differed was GENERICITY AND THE `component` SLOT: `@object-ui/types`' was
* non-generic with `component: any`, this one was `<T = any>` with
* `component: ComponentRenderer<T>`.
*
* ⚠️ Measured on the EMITTED `.d.ts` of both packages immediately before this
* convergence, `Exact<TypesConfig, CoreConfig>` — mutual assignability — read
* `true` on the DIVERGED pair, because `component: any` absorbs everything and
* every other member is optional. An assignability assertion is a GHOST here,
* exactly as `__tests__/component-meta-derives-from-canonical.test.ts` records
* for the sibling type. The readings that actually moved were "is
* `@object-ui/types`' declaration generic" (`TS2315: Type 'ComponentConfig' is
* not generic` before, no error after) and the symmetric key-set difference.
*
* A re-export is not a second authority — `scripts/__tests__/one-authority-per-exported-name-6273.test.ts`
* counts declarations and ALIASING re-exports, never `export type { X } from …`
* — which is why this convergence takes `ComponentConfig` off that gate's
* `KNOWN_COLLISIONS` baseline. Deriving a new declaration here instead would
* NOT have: `ComponentMeta` was converged that way by PR #6297 and is still a
* row on that baseline today.
*/
export type { ComponentConfig } from '@object-ui/types';

/**
* What the registry actually STORES and hands back — `ComponentConfig` plus the
* registry-only keys, as a NAMED extension (objectui#6298).
*
* This is type-identical to the `ComponentConfig` this file used to declare:
* `CanonicalComponentMeta & RegistryComponentMetaExtras & { type; component }`,
* reached from the other side. The two halves are named rather than restated —
* one declaration for the shared members ({@link ComponentConfig}, in
* `@object-ui/types`), a named extension for the rest
* ({@link RegistryComponentMetaExtras}) — which is the shape PR #6297 gave
* {@link ComponentMeta}.
*
* It exists because the extras are NOT optional decoration on a registry entry:
* {@link Registry.getNamespaceComponents} filters on `config.namespace`, and
* `tier` / `labelling` / `deprecated` are read off registrations elsewhere. A
* bare re-export as the entry type would have silently dropped them.
*
* ⚠️ `ComponentConfig` remains the AUTHORING vocabulary and the general name;
* registrations are checked against {@link ComponentMeta}, never against this.
* Nothing writes a `RegistryComponentConfig` literal — the registry builds them.
*/
export type RegistryComponentConfig<T = any> = ComponentConfig<T> &
RegistryComponentMetaExtras;

/**
* A CONTRACT-surface entry (ADR-0080), as returned by
* {@link Registry.getPublicConfigs}.
*
* Same shape as {@link ComponentConfig} except `component` is absent while the
* Same shape as {@link RegistryComponentConfig} except `component` is absent while the
* entry is still a pending `registerLazy` stub: the plugin module has not been
* imported yet, so there is no renderer to hand out. Consumers render such an
* entry through `SchemaRenderer`, which triggers the loader and shows a
Expand DownExpand Up@@ -314,7 +395,7 @@ export function withElementDataSourceInput<T>(
}

export class Registry<T = any> {
private components = new Map<string, ComponentConfig<T>>();
private components = new Map<string, RegistryComponentConfig<T>>();
private lazyEntries = new Map<string, LazyEntry>();
/**
* Notifies subscribers that the registry has changed (new components
Expand DownExpand Up@@ -565,7 +646,7 @@ export class Registry<T = any> {
* @param namespace - Optional namespace for lookup priority
* @returns Component configuration or undefined
*/
getConfig(type: string, namespace?: string): ComponentConfig<T> | undefined {
getConfig(type: string, namespace?: string): RegistryComponentConfig<T> | undefined {
// If namespace is explicitly provided, ONLY look in that namespace (no fallback)
if (namespace) {
const namespacedType = `${namespace}:${type}`;
Expand DownExpand Up@@ -686,7 +767,7 @@ export class Registry<T = any> {
*
* @returns Array of all component configurations
*/
getAllConfigs(): ComponentConfig<T>[] {
getAllConfigs(): RegistryComponentConfig<T>[] {
return Array.from(this.components.values());
}

Expand DownExpand Up@@ -755,7 +836,7 @@ export class Registry<T = any> {
* @param namespace - Namespace to filter by
* @returns Array of component configurations in the namespace
*/
getNamespaceComponents(namespace: string): ComponentConfig<T>[] {
getNamespaceComponents(namespace: string): RegistryComponentConfig<T>[] {
return Array.from(this.components.values()).filter(
config => config.namespace === namespace
);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(types,core): one authority for `ComponentConfig` by claude[bot] · Pull Request #6937 · objectstack-ai/objectui · GitHub
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
32 changes: 32 additions & 0 deletions .changeset/6298-componentconfig-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
---
'@object-ui/types': minor
'@object-ui/core': minor
---

`ComponentConfig` now has one authority: `@object-ui/types` declares it, `@object-ui/core` re-exports it

`@object-ui/types` and `@object-ui/core` each published a declaration of
`ComponentConfig`, so an auto-import picked between two different types by
alphabetical order. After the `ComponentMeta` convergence the remaining
difference was genericity and the `component` slot: `@object-ui/types`' was
non-generic with `component: any`, core's was `<T = any>` with
`component: ComponentRenderer<T>`.

`@object-ui/types`' declaration gains that type parameter, **defaulted**, so
every existing spelling keeps its meaning exactly — bare `ComponentConfig` is
`ComponentConfig<any>`, whose `component` is `any`, as before. `@object-ui/core`
re-exports it instead of declaring its own.

The registry-only keys (`tier`, `namespace`, `skipFallback`, `labelling`,
`deprecated`) were not dropped: they moved to a named extension,
`RegistryComponentConfig`, which is what `Registry.getConfig`,
`getAllConfigs` and `getNamespaceComponents` return. Those return values are
type-identical to what they returned before, so every read path is unchanged.

**Breaking:** a consumer that imports `ComponentConfig` from `@object-ui/core`
*and* touches one of those five registry-only keys through that annotation must
switch the annotation to `RegistryComponentConfig` — the name `ComponentConfig`
no longer carries them there. Filed `minor` rather than `major` per AGENTS.md's
versioning policy: objectui's own breaking changes ship as `minor` with the break
spelled out here, because the whole publishable set is one changeset `fixed` group
pinned to `@objectstack`'s major.
2 changes: 1 addition & 1 deletion content/docs/guide/plugin-development.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,7 +348,7 @@ import { ComponentRegistry } from '@object-ui/core';

ComponentRegistry.has('board'); // boolean
ComponentRegistry.getAllTypes(); // string[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // ComponentConfig[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // RegistryComponentConfig[]
```

## Plugin Configuration & Schema Types
Expand Down
99 changes: 90 additions & 9 deletions packages/core/src/registry/Registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,12 +7,38 @@
*/

import type { ComponentMeta as CanonicalComponentMeta } from '@object-ui/types';
// Deliberately a SECOND import statement from the same module rather than a
// widening of the line above: `__tests__/component-meta-derives-from-canonical.test.ts`
// pins that line as an exact string (objectui#6067's source-identity assertion),
// so adding a specifier to it would red a pin that has nothing to do with this
// name. Two `import type` lines from one module is legal and costs nothing —
// type imports are erased.
import type { ComponentConfig } from '@object-ui/types';
import {
ELEMENT_DATA_SOURCE_INPUT,
isElementDataSourceBlock,
} from '../data-scope/element-data-source.js';
import { PUBLIC_BLOCKS } from './public-blocks.js';

/**
* The renderer a registration carries — the IDENTITY alias, deliberately.
*
* ⚠️ Load-bearing for objectui#6298, which is why it is documented rather than
* left as a bare line. `ComponentRenderer<T>` resolves to `T` and contributes
* NO type information: that is the entire reason `@object-ui/types` can declare
* the one `ComponentConfig` with `component: T` and mean exactly what this
* package used to mean by `component: ComponentRenderer<T>`, WITHOUT
* `@object-ui/types` needing to reach this declaration. It could not have
* reached it: `@object-ui/types` is the bottom layer (`packages/types/package.json`
* depends on `@objectstack/spec` and `zod` only) and this package depends on it,
* so an edge in that direction would be a cycle.
*
* The alias survives as the NAME this package's own renderer-shaped positions
* are spelled with ({@link withElementDataSourceInput}, {@link PublicComponentConfig}).
* If it is ever given real content, `component: T` over in `@object-ui/types`
* stops being the same slot — `__tests__/component-config-single-declaration.test.ts`
* asserts the identity so that change cannot pass unnoticed.
*/
export type ComponentRenderer<T = any> = T;

/**
Expand DownExpand Up@@ -233,16 +259,71 @@ export type RegistryComponentMetaExtras = {
*/
export type ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras;

export type ComponentConfig<T = any> = ComponentMeta & {
type: string;
component: ComponentRenderer<T>;
};
/**
* ONE authority for `ComponentConfig` (objectui#6298) — this package RE-EXPORTS
* `@object-ui/types`' declaration instead of declaring a second one, the same
* disposition objectui#5671 gave `ComponentInput` a few lines above and
* objectui#4580 ruled for the whole family: *a structural copy would reproduce
* the defect the moment either side moved.*
*
* ## What was wrong
*
* Both spellings were PUBLISHED — `@object-ui/types`' `src/index.ts` exports its
* one, and this file reaches `@object-ui/core`'s public entry through
* `src/index.ts`'s `export * from './registry/Registry.js'`. An IDE auto-import
* therefore picked between two different types by alphabetical order. After
* objectui#6067 / PR #6297 single-sourced the `ComponentMeta` half, what still
* differed was GENERICITY AND THE `component` SLOT: `@object-ui/types`' was
* non-generic with `component: any`, this one was `<T = any>` with
* `component: ComponentRenderer<T>`.
*
* ⚠️ Measured on the EMITTED `.d.ts` of both packages immediately before this
* convergence, `Exact<TypesConfig, CoreConfig>` — mutual assignability — read
* `true` on the DIVERGED pair, because `component: any` absorbs everything and
* every other member is optional. An assignability assertion is a GHOST here,
* exactly as `__tests__/component-meta-derives-from-canonical.test.ts` records
* for the sibling type. The readings that actually moved were "is
* `@object-ui/types`' declaration generic" (`TS2315: Type 'ComponentConfig' is
* not generic` before, no error after) and the symmetric key-set difference.
*
* A re-export is not a second authority — `scripts/__tests__/one-authority-per-exported-name-6273.test.ts`
* counts declarations and ALIASING re-exports, never `export type { X } from …`
* — which is why this convergence takes `ComponentConfig` off that gate's
* `KNOWN_COLLISIONS` baseline. Deriving a new declaration here instead would
* NOT have: `ComponentMeta` was converged that way by PR #6297 and is still a
* row on that baseline today.
*/
export type { ComponentConfig } from '@object-ui/types';

/**
* What the registry actually STORES and hands back — `ComponentConfig` plus the
* registry-only keys, as a NAMED extension (objectui#6298).
*
* This is type-identical to the `ComponentConfig` this file used to declare:
* `CanonicalComponentMeta & RegistryComponentMetaExtras & { type; component }`,
* reached from the other side. The two halves are named rather than restated —
* one declaration for the shared members ({@link ComponentConfig}, in
* `@object-ui/types`), a named extension for the rest
* ({@link RegistryComponentMetaExtras}) — which is the shape PR #6297 gave
* {@link ComponentMeta}.
*
* It exists because the extras are NOT optional decoration on a registry entry:
* {@link Registry.getNamespaceComponents} filters on `config.namespace`, and
* `tier` / `labelling` / `deprecated` are read off registrations elsewhere. A
* bare re-export as the entry type would have silently dropped them.
*
* ⚠️ `ComponentConfig` remains the AUTHORING vocabulary and the general name;
* registrations are checked against {@link ComponentMeta}, never against this.
* Nothing writes a `RegistryComponentConfig` literal — the registry builds them.
*/
export type RegistryComponentConfig<T = any> = ComponentConfig<T> &
RegistryComponentMetaExtras;

/**
* A CONTRACT-surface entry (ADR-0080), as returned by
* {@link Registry.getPublicConfigs}.
*
* Same shape as {@link ComponentConfig} except `component` is absent while the
* Same shape as {@link RegistryComponentConfig} except `component` is absent while the
* entry is still a pending `registerLazy` stub: the plugin module has not been
* imported yet, so there is no renderer to hand out. Consumers render such an
* entry through `SchemaRenderer`, which triggers the loader and shows a
Expand DownExpand Up@@ -314,7 +395,7 @@ export function withElementDataSourceInput<T>(
}

export class Registry<T = any> {
private components = new Map<string, ComponentConfig<T>>();
private components = new Map<string, RegistryComponentConfig<T>>();
private lazyEntries = new Map<string, LazyEntry>();
/**
* Notifies subscribers that the registry has changed (new components
Expand DownExpand Up@@ -565,7 +646,7 @@ export class Registry<T = any> {
* @param namespace - Optional namespace for lookup priority
* @returns Component configuration or undefined
*/
getConfig(type: string, namespace?: string): ComponentConfig<T> | undefined {
getConfig(type: string, namespace?: string): RegistryComponentConfig<T> | undefined {
// If namespace is explicitly provided, ONLY look in that namespace (no fallback)
if (namespace) {
const namespacedType = `${namespace}:${type}`;
Expand DownExpand Up@@ -686,7 +767,7 @@ export class Registry<T = any> {
*
* @returns Array of all component configurations
*/
getAllConfigs(): ComponentConfig<T>[] {
getAllConfigs(): RegistryComponentConfig<T>[] {
return Array.from(this.components.values());
}

Expand DownExpand Up@@ -755,7 +836,7 @@ export class Registry<T = any> {
* @param namespace - Namespace to filter by
* @returns Array of component configurations in the namespace
*/
getNamespaceComponents(namespace: string): ComponentConfig<T>[] {
getNamespaceComponents(namespace: string): RegistryComponentConfig<T>[] {
return Array.from(this.components.values()).filter(
config => config.namespace === namespace
);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(types,core): one authority for `ComponentConfig` by claude[bot] · Pull Request #6937 · objectstack-ai/objectui · GitHub
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
32 changes: 32 additions & 0 deletions .changeset/6298-componentconfig-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
---
'@object-ui/types': minor
'@object-ui/core': minor
---

`ComponentConfig` now has one authority: `@object-ui/types` declares it, `@object-ui/core` re-exports it

`@object-ui/types` and `@object-ui/core` each published a declaration of
`ComponentConfig`, so an auto-import picked between two different types by
alphabetical order. After the `ComponentMeta` convergence the remaining
difference was genericity and the `component` slot: `@object-ui/types`' was
non-generic with `component: any`, core's was `<T = any>` with
`component: ComponentRenderer<T>`.

`@object-ui/types`' declaration gains that type parameter, **defaulted**, so
every existing spelling keeps its meaning exactly — bare `ComponentConfig` is
`ComponentConfig<any>`, whose `component` is `any`, as before. `@object-ui/core`
re-exports it instead of declaring its own.

The registry-only keys (`tier`, `namespace`, `skipFallback`, `labelling`,
`deprecated`) were not dropped: they moved to a named extension,
`RegistryComponentConfig`, which is what `Registry.getConfig`,
`getAllConfigs` and `getNamespaceComponents` return. Those return values are
type-identical to what they returned before, so every read path is unchanged.

**Breaking:** a consumer that imports `ComponentConfig` from `@object-ui/core`
*and* touches one of those five registry-only keys through that annotation must
switch the annotation to `RegistryComponentConfig` — the name `ComponentConfig`
no longer carries them there. Filed `minor` rather than `major` per AGENTS.md's
versioning policy: objectui's own breaking changes ship as `minor` with the break
spelled out here, because the whole publishable set is one changeset `fixed` group
pinned to `@objectstack`'s major.
2 changes: 1 addition & 1 deletion content/docs/guide/plugin-development.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,7 +348,7 @@ import { ComponentRegistry } from '@object-ui/core';

ComponentRegistry.has('board'); // boolean
ComponentRegistry.getAllTypes(); // string[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // ComponentConfig[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // RegistryComponentConfig[]
```

## Plugin Configuration & Schema Types
Expand Down
99 changes: 90 additions & 9 deletions packages/core/src/registry/Registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,12 +7,38 @@
*/

import type { ComponentMeta as CanonicalComponentMeta } from '@object-ui/types';
// Deliberately a SECOND import statement from the same module rather than a
// widening of the line above: `__tests__/component-meta-derives-from-canonical.test.ts`
// pins that line as an exact string (objectui#6067's source-identity assertion),
// so adding a specifier to it would red a pin that has nothing to do with this
// name. Two `import type` lines from one module is legal and costs nothing —
// type imports are erased.
import type { ComponentConfig } from '@object-ui/types';
import {
ELEMENT_DATA_SOURCE_INPUT,
isElementDataSourceBlock,
} from '../data-scope/element-data-source.js';
import { PUBLIC_BLOCKS } from './public-blocks.js';

/**
* The renderer a registration carries — the IDENTITY alias, deliberately.
*
* ⚠️ Load-bearing for objectui#6298, which is why it is documented rather than
* left as a bare line. `ComponentRenderer<T>` resolves to `T` and contributes
* NO type information: that is the entire reason `@object-ui/types` can declare
* the one `ComponentConfig` with `component: T` and mean exactly what this
* package used to mean by `component: ComponentRenderer<T>`, WITHOUT
* `@object-ui/types` needing to reach this declaration. It could not have
* reached it: `@object-ui/types` is the bottom layer (`packages/types/package.json`
* depends on `@objectstack/spec` and `zod` only) and this package depends on it,
* so an edge in that direction would be a cycle.
*
* The alias survives as the NAME this package's own renderer-shaped positions
* are spelled with ({@link withElementDataSourceInput}, {@link PublicComponentConfig}).
* If it is ever given real content, `component: T` over in `@object-ui/types`
* stops being the same slot — `__tests__/component-config-single-declaration.test.ts`
* asserts the identity so that change cannot pass unnoticed.
*/
export type ComponentRenderer<T = any> = T;

/**
Expand DownExpand Up@@ -233,16 +259,71 @@ export type RegistryComponentMetaExtras = {
*/
export type ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras;

export type ComponentConfig<T = any> = ComponentMeta & {
type: string;
component: ComponentRenderer<T>;
};
/**
* ONE authority for `ComponentConfig` (objectui#6298) — this package RE-EXPORTS
* `@object-ui/types`' declaration instead of declaring a second one, the same
* disposition objectui#5671 gave `ComponentInput` a few lines above and
* objectui#4580 ruled for the whole family: *a structural copy would reproduce
* the defect the moment either side moved.*
*
* ## What was wrong
*
* Both spellings were PUBLISHED — `@object-ui/types`' `src/index.ts` exports its
* one, and this file reaches `@object-ui/core`'s public entry through
* `src/index.ts`'s `export * from './registry/Registry.js'`. An IDE auto-import
* therefore picked between two different types by alphabetical order. After
* objectui#6067 / PR #6297 single-sourced the `ComponentMeta` half, what still
* differed was GENERICITY AND THE `component` SLOT: `@object-ui/types`' was
* non-generic with `component: any`, this one was `<T = any>` with
* `component: ComponentRenderer<T>`.
*
* ⚠️ Measured on the EMITTED `.d.ts` of both packages immediately before this
* convergence, `Exact<TypesConfig, CoreConfig>` — mutual assignability — read
* `true` on the DIVERGED pair, because `component: any` absorbs everything and
* every other member is optional. An assignability assertion is a GHOST here,
* exactly as `__tests__/component-meta-derives-from-canonical.test.ts` records
* for the sibling type. The readings that actually moved were "is
* `@object-ui/types`' declaration generic" (`TS2315: Type 'ComponentConfig' is
* not generic` before, no error after) and the symmetric key-set difference.
*
* A re-export is not a second authority — `scripts/__tests__/one-authority-per-exported-name-6273.test.ts`
* counts declarations and ALIASING re-exports, never `export type { X } from …`
* — which is why this convergence takes `ComponentConfig` off that gate's
* `KNOWN_COLLISIONS` baseline. Deriving a new declaration here instead would
* NOT have: `ComponentMeta` was converged that way by PR #6297 and is still a
* row on that baseline today.
*/
export type { ComponentConfig } from '@object-ui/types';

/**
* What the registry actually STORES and hands back — `ComponentConfig` plus the
* registry-only keys, as a NAMED extension (objectui#6298).
*
* This is type-identical to the `ComponentConfig` this file used to declare:
* `CanonicalComponentMeta & RegistryComponentMetaExtras & { type; component }`,
* reached from the other side. The two halves are named rather than restated —
* one declaration for the shared members ({@link ComponentConfig}, in
* `@object-ui/types`), a named extension for the rest
* ({@link RegistryComponentMetaExtras}) — which is the shape PR #6297 gave
* {@link ComponentMeta}.
*
* It exists because the extras are NOT optional decoration on a registry entry:
* {@link Registry.getNamespaceComponents} filters on `config.namespace`, and
* `tier` / `labelling` / `deprecated` are read off registrations elsewhere. A
* bare re-export as the entry type would have silently dropped them.
*
* ⚠️ `ComponentConfig` remains the AUTHORING vocabulary and the general name;
* registrations are checked against {@link ComponentMeta}, never against this.
* Nothing writes a `RegistryComponentConfig` literal — the registry builds them.
*/
export type RegistryComponentConfig<T = any> = ComponentConfig<T> &
RegistryComponentMetaExtras;

/**
* A CONTRACT-surface entry (ADR-0080), as returned by
* {@link Registry.getPublicConfigs}.
*
* Same shape as {@link ComponentConfig} except `component` is absent while the
* Same shape as {@link RegistryComponentConfig} except `component` is absent while the
* entry is still a pending `registerLazy` stub: the plugin module has not been
* imported yet, so there is no renderer to hand out. Consumers render such an
* entry through `SchemaRenderer`, which triggers the loader and shows a
Expand DownExpand Up@@ -314,7 +395,7 @@ export function withElementDataSourceInput<T>(
}

export class Registry<T = any> {
private components = new Map<string, ComponentConfig<T>>();
private components = new Map<string, RegistryComponentConfig<T>>();
private lazyEntries = new Map<string, LazyEntry>();
/**
* Notifies subscribers that the registry has changed (new components
Expand DownExpand Up@@ -565,7 +646,7 @@ export class Registry<T = any> {
* @param namespace - Optional namespace for lookup priority
* @returns Component configuration or undefined
*/
getConfig(type: string, namespace?: string): ComponentConfig<T> | undefined {
getConfig(type: string, namespace?: string): RegistryComponentConfig<T> | undefined {
// If namespace is explicitly provided, ONLY look in that namespace (no fallback)
if (namespace) {
const namespacedType = `${namespace}:${type}`;
Expand DownExpand Up@@ -686,7 +767,7 @@ export class Registry<T = any> {
*
* @returns Array of all component configurations
*/
getAllConfigs(): ComponentConfig<T>[] {
getAllConfigs(): RegistryComponentConfig<T>[] {
return Array.from(this.components.values());
}

Expand DownExpand Up@@ -755,7 +836,7 @@ export class Registry<T = any> {
* @param namespace - Namespace to filter by
* @returns Array of component configurations in the namespace
*/
getNamespaceComponents(namespace: string): ComponentConfig<T>[] {
getNamespaceComponents(namespace: string): RegistryComponentConfig<T>[] {
return Array.from(this.components.values()).filter(
config => config.namespace === namespace
);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(types,core): one authority for `ComponentConfig` by claude[bot] · Pull Request #6937 · objectstack-ai/objectui · GitHub
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
32 changes: 32 additions & 0 deletions .changeset/6298-componentconfig-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
---
'@object-ui/types': minor
'@object-ui/core': minor
---

`ComponentConfig` now has one authority: `@object-ui/types` declares it, `@object-ui/core` re-exports it

`@object-ui/types` and `@object-ui/core` each published a declaration of
`ComponentConfig`, so an auto-import picked between two different types by
alphabetical order. After the `ComponentMeta` convergence the remaining
difference was genericity and the `component` slot: `@object-ui/types`' was
non-generic with `component: any`, core's was `<T = any>` with
`component: ComponentRenderer<T>`.

`@object-ui/types`' declaration gains that type parameter, **defaulted**, so
every existing spelling keeps its meaning exactly — bare `ComponentConfig` is
`ComponentConfig<any>`, whose `component` is `any`, as before. `@object-ui/core`
re-exports it instead of declaring its own.

The registry-only keys (`tier`, `namespace`, `skipFallback`, `labelling`,
`deprecated`) were not dropped: they moved to a named extension,
`RegistryComponentConfig`, which is what `Registry.getConfig`,
`getAllConfigs` and `getNamespaceComponents` return. Those return values are
type-identical to what they returned before, so every read path is unchanged.

**Breaking:** a consumer that imports `ComponentConfig` from `@object-ui/core`
*and* touches one of those five registry-only keys through that annotation must
switch the annotation to `RegistryComponentConfig` — the name `ComponentConfig`
no longer carries them there. Filed `minor` rather than `major` per AGENTS.md's
versioning policy: objectui's own breaking changes ship as `minor` with the break
spelled out here, because the whole publishable set is one changeset `fixed` group
pinned to `@objectstack`'s major.
2 changes: 1 addition & 1 deletion content/docs/guide/plugin-development.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,7 +348,7 @@ import { ComponentRegistry } from '@object-ui/core';

ComponentRegistry.has('board'); // boolean
ComponentRegistry.getAllTypes(); // string[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // ComponentConfig[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // RegistryComponentConfig[]
```

## Plugin Configuration & Schema Types
Expand Down
99 changes: 90 additions & 9 deletions packages/core/src/registry/Registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,12 +7,38 @@
*/

import type { ComponentMeta as CanonicalComponentMeta } from '@object-ui/types';
// Deliberately a SECOND import statement from the same module rather than a
// widening of the line above: `__tests__/component-meta-derives-from-canonical.test.ts`
// pins that line as an exact string (objectui#6067's source-identity assertion),
// so adding a specifier to it would red a pin that has nothing to do with this
// name. Two `import type` lines from one module is legal and costs nothing —
// type imports are erased.
import type { ComponentConfig } from '@object-ui/types';
import {
ELEMENT_DATA_SOURCE_INPUT,
isElementDataSourceBlock,
} from '../data-scope/element-data-source.js';
import { PUBLIC_BLOCKS } from './public-blocks.js';

/**
* The renderer a registration carries — the IDENTITY alias, deliberately.
*
* ⚠️ Load-bearing for objectui#6298, which is why it is documented rather than
* left as a bare line. `ComponentRenderer<T>` resolves to `T` and contributes
* NO type information: that is the entire reason `@object-ui/types` can declare
* the one `ComponentConfig` with `component: T` and mean exactly what this
* package used to mean by `component: ComponentRenderer<T>`, WITHOUT
* `@object-ui/types` needing to reach this declaration. It could not have
* reached it: `@object-ui/types` is the bottom layer (`packages/types/package.json`
* depends on `@objectstack/spec` and `zod` only) and this package depends on it,
* so an edge in that direction would be a cycle.
*
* The alias survives as the NAME this package's own renderer-shaped positions
* are spelled with ({@link withElementDataSourceInput}, {@link PublicComponentConfig}).
* If it is ever given real content, `component: T` over in `@object-ui/types`
* stops being the same slot — `__tests__/component-config-single-declaration.test.ts`
* asserts the identity so that change cannot pass unnoticed.
*/
export type ComponentRenderer<T = any> = T;

/**
Expand DownExpand Up@@ -233,16 +259,71 @@ export type RegistryComponentMetaExtras = {
*/
export type ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras;

export type ComponentConfig<T = any> = ComponentMeta & {
type: string;
component: ComponentRenderer<T>;
};
/**
* ONE authority for `ComponentConfig` (objectui#6298) — this package RE-EXPORTS
* `@object-ui/types`' declaration instead of declaring a second one, the same
* disposition objectui#5671 gave `ComponentInput` a few lines above and
* objectui#4580 ruled for the whole family: *a structural copy would reproduce
* the defect the moment either side moved.*
*
* ## What was wrong
*
* Both spellings were PUBLISHED — `@object-ui/types`' `src/index.ts` exports its
* one, and this file reaches `@object-ui/core`'s public entry through
* `src/index.ts`'s `export * from './registry/Registry.js'`. An IDE auto-import
* therefore picked between two different types by alphabetical order. After
* objectui#6067 / PR #6297 single-sourced the `ComponentMeta` half, what still
* differed was GENERICITY AND THE `component` SLOT: `@object-ui/types`' was
* non-generic with `component: any`, this one was `<T = any>` with
* `component: ComponentRenderer<T>`.
*
* ⚠️ Measured on the EMITTED `.d.ts` of both packages immediately before this
* convergence, `Exact<TypesConfig, CoreConfig>` — mutual assignability — read
* `true` on the DIVERGED pair, because `component: any` absorbs everything and
* every other member is optional. An assignability assertion is a GHOST here,
* exactly as `__tests__/component-meta-derives-from-canonical.test.ts` records
* for the sibling type. The readings that actually moved were "is
* `@object-ui/types`' declaration generic" (`TS2315: Type 'ComponentConfig' is
* not generic` before, no error after) and the symmetric key-set difference.
*
* A re-export is not a second authority — `scripts/__tests__/one-authority-per-exported-name-6273.test.ts`
* counts declarations and ALIASING re-exports, never `export type { X } from …`
* — which is why this convergence takes `ComponentConfig` off that gate's
* `KNOWN_COLLISIONS` baseline. Deriving a new declaration here instead would
* NOT have: `ComponentMeta` was converged that way by PR #6297 and is still a
* row on that baseline today.
*/
export type { ComponentConfig } from '@object-ui/types';

/**
* What the registry actually STORES and hands back — `ComponentConfig` plus the
* registry-only keys, as a NAMED extension (objectui#6298).
*
* This is type-identical to the `ComponentConfig` this file used to declare:
* `CanonicalComponentMeta & RegistryComponentMetaExtras & { type; component }`,
* reached from the other side. The two halves are named rather than restated —
* one declaration for the shared members ({@link ComponentConfig}, in
* `@object-ui/types`), a named extension for the rest
* ({@link RegistryComponentMetaExtras}) — which is the shape PR #6297 gave
* {@link ComponentMeta}.
*
* It exists because the extras are NOT optional decoration on a registry entry:
* {@link Registry.getNamespaceComponents} filters on `config.namespace`, and
* `tier` / `labelling` / `deprecated` are read off registrations elsewhere. A
* bare re-export as the entry type would have silently dropped them.
*
* ⚠️ `ComponentConfig` remains the AUTHORING vocabulary and the general name;
* registrations are checked against {@link ComponentMeta}, never against this.
* Nothing writes a `RegistryComponentConfig` literal — the registry builds them.
*/
export type RegistryComponentConfig<T = any> = ComponentConfig<T> &
RegistryComponentMetaExtras;

/**
* A CONTRACT-surface entry (ADR-0080), as returned by
* {@link Registry.getPublicConfigs}.
*
* Same shape as {@link ComponentConfig} except `component` is absent while the
* Same shape as {@link RegistryComponentConfig} except `component` is absent while the
* entry is still a pending `registerLazy` stub: the plugin module has not been
* imported yet, so there is no renderer to hand out. Consumers render such an
* entry through `SchemaRenderer`, which triggers the loader and shows a
Expand DownExpand Up@@ -314,7 +395,7 @@ export function withElementDataSourceInput<T>(
}

export class Registry<T = any> {
private components = new Map<string, ComponentConfig<T>>();
private components = new Map<string, RegistryComponentConfig<T>>();
private lazyEntries = new Map<string, LazyEntry>();
/**
* Notifies subscribers that the registry has changed (new components
Expand DownExpand Up@@ -565,7 +646,7 @@ export class Registry<T = any> {
* @param namespace - Optional namespace for lookup priority
* @returns Component configuration or undefined
*/
getConfig(type: string, namespace?: string): ComponentConfig<T> | undefined {
getConfig(type: string, namespace?: string): RegistryComponentConfig<T> | undefined {
// If namespace is explicitly provided, ONLY look in that namespace (no fallback)
if (namespace) {
const namespacedType = `${namespace}:${type}`;
Expand DownExpand Up@@ -686,7 +767,7 @@ export class Registry<T = any> {
*
* @returns Array of all component configurations
*/
getAllConfigs(): ComponentConfig<T>[] {
getAllConfigs(): RegistryComponentConfig<T>[] {
return Array.from(this.components.values());
}

Expand DownExpand Up@@ -755,7 +836,7 @@ export class Registry<T = any> {
* @param namespace - Namespace to filter by
* @returns Array of component configurations in the namespace
*/
getNamespaceComponents(namespace: string): ComponentConfig<T>[] {
getNamespaceComponents(namespace: string): RegistryComponentConfig<T>[] {
return Array.from(this.components.values()).filter(
config => config.namespace === namespace
);
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(types,core): one authority for `ComponentConfig` by claude[bot] · Pull Request #6937 · objectstack-ai/objectui · GitHub
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
32 changes: 32 additions & 0 deletions .changeset/6298-componentconfig-one-authority.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
---
'@object-ui/types': minor
'@object-ui/core': minor
---

`ComponentConfig` now has one authority: `@object-ui/types` declares it, `@object-ui/core` re-exports it

`@object-ui/types` and `@object-ui/core` each published a declaration of
`ComponentConfig`, so an auto-import picked between two different types by
alphabetical order. After the `ComponentMeta` convergence the remaining
difference was genericity and the `component` slot: `@object-ui/types`' was
non-generic with `component: any`, core's was `<T = any>` with
`component: ComponentRenderer<T>`.

`@object-ui/types`' declaration gains that type parameter, **defaulted**, so
every existing spelling keeps its meaning exactly — bare `ComponentConfig` is
`ComponentConfig<any>`, whose `component` is `any`, as before. `@object-ui/core`
re-exports it instead of declaring its own.

The registry-only keys (`tier`, `namespace`, `skipFallback`, `labelling`,
`deprecated`) were not dropped: they moved to a named extension,
`RegistryComponentConfig`, which is what `Registry.getConfig`,
`getAllConfigs` and `getNamespaceComponents` return. Those return values are
type-identical to what they returned before, so every read path is unchanged.

**Breaking:** a consumer that imports `ComponentConfig` from `@object-ui/core`
*and* touches one of those five registry-only keys through that annotation must
switch the annotation to `RegistryComponentConfig` — the name `ComponentConfig`
no longer carries them there. Filed `minor` rather than `major` per AGENTS.md's
versioning policy: objectui's own breaking changes ship as `minor` with the break
spelled out here, because the whole publishable set is one changeset `fixed` group
pinned to `@objectstack`'s major.
2 changes: 1 addition & 1 deletion content/docs/guide/plugin-development.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -348,7 +348,7 @@ import { ComponentRegistry } from '@object-ui/core';

ComponentRegistry.has('board'); // boolean
ComponentRegistry.getAllTypes(); // string[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // ComponentConfig[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // RegistryComponentConfig[]
```

## Plugin Configuration & Schema Types
Expand Down
99 changes: 90 additions & 9 deletions packages/core/src/registry/Registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,12 +7,38 @@
*/

import type { ComponentMeta as CanonicalComponentMeta } from '@object-ui/types';
// Deliberately a SECOND import statement from the same module rather than a
// widening of the line above: `__tests__/component-meta-derives-from-canonical.test.ts`
// pins that line as an exact string (objectui#6067's source-identity assertion),
// so adding a specifier to it would red a pin that has nothing to do with this
// name. Two `import type` lines from one module is legal and costs nothing —
// type imports are erased.
import type { ComponentConfig } from '@object-ui/types';
import {
ELEMENT_DATA_SOURCE_INPUT,
isElementDataSourceBlock,
} from '../data-scope/element-data-source.js';
import { PUBLIC_BLOCKS } from './public-blocks.js';

/**
* The renderer a registration carries — the IDENTITY alias, deliberately.
*
* ⚠️ Load-bearing for objectui#6298, which is why it is documented rather than
* left as a bare line. `ComponentRenderer<T>` resolves to `T` and contributes
* NO type information: that is the entire reason `@object-ui/types` can declare
* the one `ComponentConfig` with `component: T` and mean exactly what this
* package used to mean by `component: ComponentRenderer<T>`, WITHOUT
* `@object-ui/types` needing to reach this declaration. It could not have
* reached it: `@object-ui/types` is the bottom layer (`packages/types/package.json`
* depends on `@objectstack/spec` and `zod` only) and this package depends on it,
* so an edge in that direction would be a cycle.
*
* The alias survives as the NAME this package's own renderer-shaped positions
* are spelled with ({@link withElementDataSourceInput}, {@link PublicComponentConfig}).
* If it is ever given real content, `component: T` over in `@object-ui/types`
* stops being the same slot — `__tests__/component-config-single-declaration.test.ts`
* asserts the identity so that change cannot pass unnoticed.
*/
export type ComponentRenderer<T = any> = T;

/**
Expand DownExpand Up@@ -233,16 +259,71 @@ export type RegistryComponentMetaExtras = {
*/
export type ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras;

export type ComponentConfig<T = any> = ComponentMeta & {
type: string;
component: ComponentRenderer<T>;
};
/**
* ONE authority for `ComponentConfig` (objectui#6298) — this package RE-EXPORTS
* `@object-ui/types`' declaration instead of declaring a second one, the same
* disposition objectui#5671 gave `ComponentInput` a few lines above and
* objectui#4580 ruled for the whole family: *a structural copy would reproduce
* the defect the moment either side moved.*
*
* ## What was wrong
*
* Both spellings were PUBLISHED — `@object-ui/types`' `src/index.ts` exports its
* one, and this file reaches `@object-ui/core`'s public entry through
* `src/index.ts`'s `export * from './registry/Registry.js'`. An IDE auto-import
* therefore picked between two different types by alphabetical order. After
* objectui#6067 / PR #6297 single-sourced the `ComponentMeta` half, what still
* differed was GENERICITY AND THE `component` SLOT: `@object-ui/types`' was
* non-generic with `component: any`, this one was `<T = any>` with
* `component: ComponentRenderer<T>`.
*
* ⚠️ Measured on the EMITTED `.d.ts` of both packages immediately before this
* convergence, `Exact<TypesConfig, CoreConfig>` — mutual assignability — read
* `true` on the DIVERGED pair, because `component: any` absorbs everything and
* every other member is optional. An assignability assertion is a GHOST here,
* exactly as `__tests__/component-meta-derives-from-canonical.test.ts` records
* for the sibling type. The readings that actually moved were "is
* `@object-ui/types`' declaration generic" (`TS2315: Type 'ComponentConfig' is
* not generic` before, no error after) and the symmetric key-set difference.
*
* A re-export is not a second authority — `scripts/__tests__/one-authority-per-exported-name-6273.test.ts`
* counts declarations and ALIASING re-exports, never `export type { X } from …`
* — which is why this convergence takes `ComponentConfig` off that gate's
* `KNOWN_COLLISIONS` baseline. Deriving a new declaration here instead would
* NOT have: `ComponentMeta` was converged that way by PR #6297 and is still a
* row on that baseline today.
*/
export type { ComponentConfig } from '@object-ui/types';

/**
* What the registry actually STORES and hands back — `ComponentConfig` plus the
* registry-only keys, as a NAMED extension (objectui#6298).
*
* This is type-identical to the `ComponentConfig` this file used to declare:
* `CanonicalComponentMeta & RegistryComponentMetaExtras & { type; component }`,
* reached from the other side. The two halves are named rather than restated —
* one declaration for the shared members ({@link ComponentConfig}, in
* `@object-ui/types`), a named extension for the rest
* ({@link RegistryComponentMetaExtras}) — which is the shape PR #6297 gave
* {@link ComponentMeta}.
*
* It exists because the extras are NOT optional decoration on a registry entry:
* {@link Registry.getNamespaceComponents} filters on `config.namespace`, and
* `tier` / `labelling` / `deprecated` are read off registrations elsewhere. A
* bare re-export as the entry type would have silently dropped them.
*
* ⚠️ `ComponentConfig` remains the AUTHORING vocabulary and the general name;
* registrations are checked against {@link ComponentMeta}, never against this.
* Nothing writes a `RegistryComponentConfig` literal — the registry builds them.
*/
export type RegistryComponentConfig<T = any> = ComponentConfig<T> &
RegistryComponentMetaExtras;

/**
* A CONTRACT-surface entry (ADR-0080), as returned by
* {@link Registry.getPublicConfigs}.
*
* Same shape as {@link ComponentConfig} except `component` is absent while the
* Same shape as {@link RegistryComponentConfig} except `component` is absent while the
* entry is still a pending `registerLazy` stub: the plugin module has not been
* imported yet, so there is no renderer to hand out. Consumers render such an
* entry through `SchemaRenderer`, which triggers the loader and shows a
Expand DownExpand Up@@ -314,7 +395,7 @@ export function withElementDataSourceInput<T>(
}

export class Registry<T = any> {
private components = new Map<string, ComponentConfig<T>>();
private components = new Map<string, RegistryComponentConfig<T>>();
private lazyEntries = new Map<string, LazyEntry>();
/**
* Notifies subscribers that the registry has changed (new components
Expand DownExpand Up@@ -565,7 +646,7 @@ export class Registry<T = any> {
* @param namespace - Optional namespace for lookup priority
* @returns Component configuration or undefined
*/
getConfig(type: string, namespace?: string): ComponentConfig<T> | undefined {
getConfig(type: string, namespace?: string): RegistryComponentConfig<T> | undefined {
// If namespace is explicitly provided, ONLY look in that namespace (no fallback)
if (namespace) {
const namespacedType = `${namespace}:${type}`;
Expand DownExpand Up@@ -686,7 +767,7 @@ export class Registry<T = any> {
*
* @returns Array of all component configurations
*/
getAllConfigs(): ComponentConfig<T>[] {
getAllConfigs(): RegistryComponentConfig<T>[] {
return Array.from(this.components.values());
}

Expand DownExpand Up@@ -755,7 +836,7 @@ export class Registry<T = any> {
* @param namespace - Namespace to filter by
* @returns Array of component configurations in the namespace
*/
getNamespaceComponents(namespace: string): ComponentConfig<T>[] {
getNamespaceComponents(namespace: string): RegistryComponentConfig<T>[] {
return Array.from(this.components.values()).filter(
config => config.namespace === namespace
);
Expand Down
Loading
Loading