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
25 changes: 25 additions & 0 deletions .changeset/6674-registry-deprecation-declaration.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
---
'@object-ui/components': minor
'@object-ui/core': minor
---

Component deprecation is now DECLARED, not just warned about (objectui#6674).

A deprecated component type used to be stated in exactly two places, neither of
which a gate, a test or a type can consult: a `console.warn` string literal
inside the renderer, and the word "(Deprecated)" inside a human-readable
`label`. Both gates that touch component types ask a different question —
whether the type RESOLVES — and a deprecated type resolves, which is how one
could be authored 85 times across 27 shipped exemplars with every check green.

- `@object-ui/core` gains `ComponentDeprecation` / `AuthoringSurface` and the
`deprecated` key on the registration metadata, plus
`ComponentRegistry.deprecationFor(type, surface)` to read it back. The
declaration carries the SURFACES it applies to rather than being a boolean:
`div` and `span` are deprecated on the JSON authoring surface and are at the
same time permanent vocabulary of the `kind:'html'` tier, so a bare flag would
be false for one of its two readers.
- `@object-ui/components` marks `div` and `span` with the declaration their
console notices already state. Nothing new is deprecated and no build starts
failing: the catalog ratchet keeps the existing stock frozen, and draining it
stays objectui#3965's worklist.
127 changes: 100 additions & 27 deletions examples/schema-catalog/test/deprecated-component-types.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,14 +28,33 @@
* and walks `content/docs` and nothing else (`DOCS_ROOT = 'content/docs'`),
* so `examples/**` is outside its scan surface entirely.
*
* It is worse than the usual declared-but-unenforced shape: the deprecation is
* not declared anywhere MACHINE-READABLE. `RegistryComponentMetaExtras` carries
* `tier` / `namespace` / `skipFallback` / `labelAssociation` and has no
* `deprecated` field, so the only statements of it are a `console.warn` string
* literal in `div.tsx` / `span.tsx` and the human-readable label
* `'Container (Deprecated)'`. `DEPRECATED_TYPES` below is therefore a hand-kept
* mirror, and `the deprecation this ratchet mirrors is still declared` is the
* arm that stops the mirror from outliving the thing it mirrors.
* It was worse than the usual declared-but-unenforced shape: when this file
* landed, the deprecation was not declared anywhere MACHINE-READABLE.
* `RegistryComponentMetaExtras` carried `tier` / `namespace` / `skipFallback` /
* `labelling` and no `deprecated` field, so the only statements of it were a
* `console.warn` string literal in `div.tsx` / `span.tsx` and the
* human-readable label `'Container (Deprecated)'`. This file was therefore
* built on a hand-kept mirror whose premise arm READ THE RENDERER'S SOURCE and
* regex-matched that console literal — the closest thing to asking "is this
* type deprecated?" that existed.
*
* ## What objectui#6674 changed, and what it did not
*
* The registration now DECLARES it: `deprecated: { surfaces: ['json'],
* replacement: … }`, read back through `ComponentRegistry.deprecationFor(type,
* surface)`. So `the deprecation this ratchet mirrors is still declared` asks
* the registry instead of grepping a `.tsx` for a console string, and
* `no LOADED registration declares a deprecation this list omits` is the new
* arm that direction makes possible at all.
*
* ⚠️ `DEPRECATED_TYPES` stays HAND-KEPT on purpose, and deriving it wholesale
* from the registry would be a regression rather than the obvious next step.
* This file loads `@object-ui/components` and nothing else; a type declared
* deprecated by a plugin package it does not import would silently drop out of
* a derived list, and the census would shrink to green. The list is the
* ratchet's authority precisely because it is complete by construction. What
* the declaration buys is that the list can now be CHECKED — in both directions
* — against something a machine can read.
*
* ## Why this ratchet freezes the stock instead of demanding zero
*
Expand DownExpand Up@@ -114,17 +133,30 @@
import { describe, it, expect } from 'vitest';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
// Registers `div` / `span` (and the rest of the basic set) at module scope, so
// the two arms below can ASK the registry what is deprecated instead of reading
// a renderer's source. Module scope, not a hook — objectui#3010/#3021.
import '@object-ui/components';
import { ComponentRegistry } from '@object-ui/core';

/** Resolved off this module, so the gate does not depend on the process cwd. */
const SCHEMAS_ROOT = fileURLToPath(new URL('../src/schemas', import.meta.url));
const COMPONENTS_SRC = fileURLToPath(
new URL('../../../packages/components/src/renderers/basic', import.meta.url),
);

/**
* The deprecated JSON-authored component types, mirrored by hand from the
* renderers' notices because the registry carries no `deprecated` flag. Kept
* honest by `the deprecation this ratchet mirrors is still declared`.
* The surface this corpus is authored on. Every fixture under `SCHEMAS_ROOT` is
* JSON metadata, so the question this ratchet asks the registry is scoped to
* it: `div` and `span` are ALSO permanent vocabulary of the `kind:'html'` tier
* (objectui#4000), where the parser compiles the plain tag straight through and
* no other spelling exists to migrate to. A gate that dropped the scope would
* be refusing a spelling that is correct on the other surface.
*/
const CORPUS_SURFACE = 'json' as const;

/**
* The deprecated JSON-authored component types this ratchet refuses. Hand-kept
* — see the header for why deriving it from the registry would shrink the
* census silently — and now checked in BOTH directions against the
* machine-readable declaration the registrations carry (objectui#6674).
*/
const DEPRECATED_TYPES = ['div', 'span'] as const;

Expand DownExpand Up@@ -293,19 +325,60 @@ describe('deprecated component types in the catalog are ratcheted (#3965)', () =

it('the deprecation this ratchet mirrors is still declared', () => {
// The mirror's premise. If a type is UN-deprecated, this file must die
// loudly rather than keep refusing a spelling that became legal again;
// `DEPRECATED_TYPES` is hand-kept precisely because the registry carries no
// machine-readable flag to derive it from.
for (const type of DEPRECATED_TYPES) {
const source = readFileSync(`${COMPONENTS_SRC}/${type}.tsx`, 'utf8');
expect(
source,
`renderers/basic/${type}.tsx no longer declares its deprecation notice. ` +
`Either the type was un-deprecated — in which case drop it from ` +
`DEPRECATED_TYPES and retire the matching baseline — or the notice ` +
`moved and this mirror needs re-pointing.`,
).toContain(`The "${type}" component is deprecated`);
}
// loudly rather than keep refusing a spelling that became legal again.
//
// This arm used to `readFileSync` the renderer and regex-match its
// `console.warn` literal, because that string was one of only two places a
// deprecation was stated and the only one a test could reach. It now asks
// the registry, which is objectui#6674's whole delivery: the question "is
// this type deprecated?" has an asker.
const undeclared = DEPRECATED_TYPES.filter(
(type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE) === undefined,
);

expect(
undeclared,
'A type this ratchet refuses no longer DECLARES a deprecation for the ' +
'json authoring surface. Either it was un-deprecated — in which case ' +
'drop it from DEPRECATED_TYPES and retire the matching baseline — or ' +
'the declaration moved and this mirror needs re-pointing. (A type ' +
'whose `surfaces` no longer lists `json` reads as un-deprecated HERE ' +
'and is still deprecated elsewhere; that is the objectui#4000 scope ' +
'working, not a bug in this arm.)',
).toEqual([]);
});

it('no LOADED registration declares a deprecation this list omits', () => {
// The direction the hand-kept mirror could never check. Before the
// declaration existed there was nothing to enumerate: a third deprecated
// type could have been added to `@object-ui/components` with a console
// string and a label, and this file would have gone on refusing exactly two.
//
// Scoped honestly to what this file LOADS — `@object-ui/components`. A
// plugin package's declaration is out of range here, which is the reason
// DEPRECATED_TYPES stays the authority rather than being derived.
//
// Non-vacuity is the arm ABOVE: an empty result here would also be produced
// by a registry that answered `undefined` for everything, and that state
// turns the premise arm red first. The two hold each other up.
const listed = new Set<string>(DEPRECATED_TYPES);
// A namespaced registration answers under BOTH spellings (`ui:div` and
// `div`); the corpus authors the bare one and the baseline is keyed on it.
// Either spelling being listed counts, and the raw key is what gets
// reported so the message names something that exists in the registry.
const bare = (key: string) => (key.includes(':') ? key.slice(key.indexOf(':') + 1) : key);
const missing = ComponentRegistry.getKnownTypes()
.filter((type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE))
.filter((type) => !listed.has(type) && !listed.has(bare(type)))
.sort();

expect(
missing,
'A loaded registration declares a json-surface deprecation that this ' +
'ratchet does not refuse. Add it to DEPRECATED_TYPES — and if the ' +
'corpus already authors it, baseline the existing stock in the same PR ' +
'rather than leaving the type unguarded.',
).toEqual([]);
});

it('the stock is exactly what this card measured, and the exemption is not a hole', () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -131,4 +132,38 @@ describe('div deprecation notice — scoped by provenance (#4000)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED, so a gate can read what the four
* cases above can only demonstrate by rendering.
*
* Before the registration carried `deprecated`, the only statements that this
* type is deprecated were the notice string literal and the word inside
* `label`; no gate, test or type could consult either, so both gates that
* touch component types ask whether the type RESOLVES instead — and it does.
*
* This case is the join. Above, the renderer EXEMPTS html-tier nodes at
* runtime; here, the registration DECLARES the identical scope. Asserting
* them in one file is what stops them drifting: widening `surfaces` to
* `['json', 'html']` without touching the exemption, or dropping the
* exemption without narrowing `surfaces`, turns this red — which no
* assertion about either one alone can do.
*/
it('DECLARES the scope those four cases demonstrate — deprecated on json, not on html', () => {
// Deprecated where the notice is fired…
expect(ComponentRegistry.deprecationFor('div', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
});

// …and NOT where the first case above proves the renderer stays silent. A
// declaration that said "deprecated" full stop would be false for the html
// tier, which is the objectui#4000 ruling this pair encodes.
expect(ComponentRegistry.deprecationFor('div', 'html')).toBeUndefined();

// The bare and namespaced spellings answer alike, because a corpus authors
// whichever it likes and the gate must not have to know which.
expect(ComponentRegistry.deprecationFor('ui:div', 'json')).toBeDefined();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -158,4 +159,25 @@ describe('span deprecation notice — scoped by provenance (#4917)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED. Level with the sibling case in
* `div-deprecation-provenance.test.tsx`, for the reason objectui#4917 gave
* for bringing this renderer level in the first place: the two carry the same
* ruling, and a fact stated for one of them and not the other is how they
* diverge.
*
* The runtime exemption above and the declaration below are the same fact.
* Moving either alone turns this red.
*/
it('DECLARES the scope those cases demonstrate — deprecated on json, not on html', () => {
expect(ComponentRegistry.deprecationFor('span', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
});

expect(ComponentRegistry.deprecationFor('span', 'html')).toBeUndefined();
expect(ComponentRegistry.deprecationFor('ui:span', 'json')).toBeDefined();
});
});
28 changes: 27 additions & 1 deletion packages/components/src/renderers/basic/div.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,11 +103,37 @@ const DivRenderer = forwardRef<HTMLDivElement, { schema: DivSchema; className?:
}
);

ComponentRegistry.register('div',
ComponentRegistry.register('div',
DivRenderer,
{
namespace: 'ui',
label: 'Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674).
*
* Until this key existed, the only two statements that this type is
* deprecated were `DIV_DEPRECATION_NOTICE` — a string literal inside a
* renderer — and the word inside `label`. Neither can be consulted by a
* gate, a test or a type, which is why a deprecated type could be authored
* 85 times across 27 shipped exemplars with every check in the repository
* green: both gates that touch component types ask whether the type
* RESOLVES, and this one resolves.
*
* `surfaces` carries the objectui#4000 ruling rather than restating it in a
* second place: the `isHtmlTierNode` exemption ABOVE and this list are the
* same fact, and `__tests__/div-deprecation-provenance.test.tsx` pins them
* to each other so neither can move alone.
*
* ⛔ Declaring this deprecates NOTHING NEW and fails NO build. The catalog
* ratchet (`examples/schema-catalog/test/deprecated-component-types.test.ts`,
* objectui#6732) freezes the existing stock and refuses growth; draining it
* is objectui#3965's worklist.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
Expand Down
18 changes: 17 additions & 1 deletion packages/components/src/renderers/basic/span.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -146,11 +146,27 @@ const SpanRenderer = forwardRef<HTMLSpanElement, { schema: TextSpanSchema; class
}
);

ComponentRegistry.register('span',
ComponentRegistry.register('span',
SpanRenderer,
{
namespace: 'ui',
label: 'Inline Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674),
* level with the sibling `div` registration for the same reason
* objectui#4917 brought this renderer level with it: `SPAN_DEPRECATION_NOTICE`
* is a string literal and `label` is prose, and no gate can read either.
*
* `surfaces` carries the objectui#4000 ruling — deprecated on the JSON
* authoring surface, permanent vocabulary of the `kind:'html'` tier — as the
* same fact the `isHtmlTierNode` exemption above applies at runtime.
* `__tests__/span-deprecation-provenance.test.tsx` pins the two together.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
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" + '
feat(core): declare component deprecation in registry metadata, readable by a gate by claude[bot] · Pull Request #6822 · 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
25 changes: 25 additions & 0 deletions .changeset/6674-registry-deprecation-declaration.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
---
'@object-ui/components': minor
'@object-ui/core': minor
---

Component deprecation is now DECLARED, not just warned about (objectui#6674).

A deprecated component type used to be stated in exactly two places, neither of
which a gate, a test or a type can consult: a `console.warn` string literal
inside the renderer, and the word "(Deprecated)" inside a human-readable
`label`. Both gates that touch component types ask a different question —
whether the type RESOLVES — and a deprecated type resolves, which is how one
could be authored 85 times across 27 shipped exemplars with every check green.

- `@object-ui/core` gains `ComponentDeprecation` / `AuthoringSurface` and the
`deprecated` key on the registration metadata, plus
`ComponentRegistry.deprecationFor(type, surface)` to read it back. The
declaration carries the SURFACES it applies to rather than being a boolean:
`div` and `span` are deprecated on the JSON authoring surface and are at the
same time permanent vocabulary of the `kind:'html'` tier, so a bare flag would
be false for one of its two readers.
- `@object-ui/components` marks `div` and `span` with the declaration their
console notices already state. Nothing new is deprecated and no build starts
failing: the catalog ratchet keeps the existing stock frozen, and draining it
stays objectui#3965's worklist.
127 changes: 100 additions & 27 deletions examples/schema-catalog/test/deprecated-component-types.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,14 +28,33 @@
* and walks `content/docs` and nothing else (`DOCS_ROOT = 'content/docs'`),
* so `examples/**` is outside its scan surface entirely.
*
* It is worse than the usual declared-but-unenforced shape: the deprecation is
* not declared anywhere MACHINE-READABLE. `RegistryComponentMetaExtras` carries
* `tier` / `namespace` / `skipFallback` / `labelAssociation` and has no
* `deprecated` field, so the only statements of it are a `console.warn` string
* literal in `div.tsx` / `span.tsx` and the human-readable label
* `'Container (Deprecated)'`. `DEPRECATED_TYPES` below is therefore a hand-kept
* mirror, and `the deprecation this ratchet mirrors is still declared` is the
* arm that stops the mirror from outliving the thing it mirrors.
* It was worse than the usual declared-but-unenforced shape: when this file
* landed, the deprecation was not declared anywhere MACHINE-READABLE.
* `RegistryComponentMetaExtras` carried `tier` / `namespace` / `skipFallback` /
* `labelling` and no `deprecated` field, so the only statements of it were a
* `console.warn` string literal in `div.tsx` / `span.tsx` and the
* human-readable label `'Container (Deprecated)'`. This file was therefore
* built on a hand-kept mirror whose premise arm READ THE RENDERER'S SOURCE and
* regex-matched that console literal — the closest thing to asking "is this
* type deprecated?" that existed.
*
* ## What objectui#6674 changed, and what it did not
*
* The registration now DECLARES it: `deprecated: { surfaces: ['json'],
* replacement: … }`, read back through `ComponentRegistry.deprecationFor(type,
* surface)`. So `the deprecation this ratchet mirrors is still declared` asks
* the registry instead of grepping a `.tsx` for a console string, and
* `no LOADED registration declares a deprecation this list omits` is the new
* arm that direction makes possible at all.
*
* ⚠️ `DEPRECATED_TYPES` stays HAND-KEPT on purpose, and deriving it wholesale
* from the registry would be a regression rather than the obvious next step.
* This file loads `@object-ui/components` and nothing else; a type declared
* deprecated by a plugin package it does not import would silently drop out of
* a derived list, and the census would shrink to green. The list is the
* ratchet's authority precisely because it is complete by construction. What
* the declaration buys is that the list can now be CHECKED — in both directions
* — against something a machine can read.
*
* ## Why this ratchet freezes the stock instead of demanding zero
*
Expand DownExpand Up@@ -114,17 +133,30 @@
import { describe, it, expect } from 'vitest';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
// Registers `div` / `span` (and the rest of the basic set) at module scope, so
// the two arms below can ASK the registry what is deprecated instead of reading
// a renderer's source. Module scope, not a hook — objectui#3010/#3021.
import '@object-ui/components';
import { ComponentRegistry } from '@object-ui/core';

/** Resolved off this module, so the gate does not depend on the process cwd. */
const SCHEMAS_ROOT = fileURLToPath(new URL('../src/schemas', import.meta.url));
const COMPONENTS_SRC = fileURLToPath(
new URL('../../../packages/components/src/renderers/basic', import.meta.url),
);

/**
* The deprecated JSON-authored component types, mirrored by hand from the
* renderers' notices because the registry carries no `deprecated` flag. Kept
* honest by `the deprecation this ratchet mirrors is still declared`.
* The surface this corpus is authored on. Every fixture under `SCHEMAS_ROOT` is
* JSON metadata, so the question this ratchet asks the registry is scoped to
* it: `div` and `span` are ALSO permanent vocabulary of the `kind:'html'` tier
* (objectui#4000), where the parser compiles the plain tag straight through and
* no other spelling exists to migrate to. A gate that dropped the scope would
* be refusing a spelling that is correct on the other surface.
*/
const CORPUS_SURFACE = 'json' as const;

/**
* The deprecated JSON-authored component types this ratchet refuses. Hand-kept
* — see the header for why deriving it from the registry would shrink the
* census silently — and now checked in BOTH directions against the
* machine-readable declaration the registrations carry (objectui#6674).
*/
const DEPRECATED_TYPES = ['div', 'span'] as const;

Expand DownExpand Up@@ -293,19 +325,60 @@ describe('deprecated component types in the catalog are ratcheted (#3965)', () =

it('the deprecation this ratchet mirrors is still declared', () => {
// The mirror's premise. If a type is UN-deprecated, this file must die
// loudly rather than keep refusing a spelling that became legal again;
// `DEPRECATED_TYPES` is hand-kept precisely because the registry carries no
// machine-readable flag to derive it from.
for (const type of DEPRECATED_TYPES) {
const source = readFileSync(`${COMPONENTS_SRC}/${type}.tsx`, 'utf8');
expect(
source,
`renderers/basic/${type}.tsx no longer declares its deprecation notice. ` +
`Either the type was un-deprecated — in which case drop it from ` +
`DEPRECATED_TYPES and retire the matching baseline — or the notice ` +
`moved and this mirror needs re-pointing.`,
).toContain(`The "${type}" component is deprecated`);
}
// loudly rather than keep refusing a spelling that became legal again.
//
// This arm used to `readFileSync` the renderer and regex-match its
// `console.warn` literal, because that string was one of only two places a
// deprecation was stated and the only one a test could reach. It now asks
// the registry, which is objectui#6674's whole delivery: the question "is
// this type deprecated?" has an asker.
const undeclared = DEPRECATED_TYPES.filter(
(type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE) === undefined,
);

expect(
undeclared,
'A type this ratchet refuses no longer DECLARES a deprecation for the ' +
'json authoring surface. Either it was un-deprecated — in which case ' +
'drop it from DEPRECATED_TYPES and retire the matching baseline — or ' +
'the declaration moved and this mirror needs re-pointing. (A type ' +
'whose `surfaces` no longer lists `json` reads as un-deprecated HERE ' +
'and is still deprecated elsewhere; that is the objectui#4000 scope ' +
'working, not a bug in this arm.)',
).toEqual([]);
});

it('no LOADED registration declares a deprecation this list omits', () => {
// The direction the hand-kept mirror could never check. Before the
// declaration existed there was nothing to enumerate: a third deprecated
// type could have been added to `@object-ui/components` with a console
// string and a label, and this file would have gone on refusing exactly two.
//
// Scoped honestly to what this file LOADS — `@object-ui/components`. A
// plugin package's declaration is out of range here, which is the reason
// DEPRECATED_TYPES stays the authority rather than being derived.
//
// Non-vacuity is the arm ABOVE: an empty result here would also be produced
// by a registry that answered `undefined` for everything, and that state
// turns the premise arm red first. The two hold each other up.
const listed = new Set<string>(DEPRECATED_TYPES);
// A namespaced registration answers under BOTH spellings (`ui:div` and
// `div`); the corpus authors the bare one and the baseline is keyed on it.
// Either spelling being listed counts, and the raw key is what gets
// reported so the message names something that exists in the registry.
const bare = (key: string) => (key.includes(':') ? key.slice(key.indexOf(':') + 1) : key);
const missing = ComponentRegistry.getKnownTypes()
.filter((type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE))
.filter((type) => !listed.has(type) && !listed.has(bare(type)))
.sort();

expect(
missing,
'A loaded registration declares a json-surface deprecation that this ' +
'ratchet does not refuse. Add it to DEPRECATED_TYPES — and if the ' +
'corpus already authors it, baseline the existing stock in the same PR ' +
'rather than leaving the type unguarded.',
).toEqual([]);
});

it('the stock is exactly what this card measured, and the exemption is not a hole', () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -131,4 +132,38 @@ describe('div deprecation notice — scoped by provenance (#4000)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED, so a gate can read what the four
* cases above can only demonstrate by rendering.
*
* Before the registration carried `deprecated`, the only statements that this
* type is deprecated were the notice string literal and the word inside
* `label`; no gate, test or type could consult either, so both gates that
* touch component types ask whether the type RESOLVES instead — and it does.
*
* This case is the join. Above, the renderer EXEMPTS html-tier nodes at
* runtime; here, the registration DECLARES the identical scope. Asserting
* them in one file is what stops them drifting: widening `surfaces` to
* `['json', 'html']` without touching the exemption, or dropping the
* exemption without narrowing `surfaces`, turns this red — which no
* assertion about either one alone can do.
*/
it('DECLARES the scope those four cases demonstrate — deprecated on json, not on html', () => {
// Deprecated where the notice is fired…
expect(ComponentRegistry.deprecationFor('div', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
});

// …and NOT where the first case above proves the renderer stays silent. A
// declaration that said "deprecated" full stop would be false for the html
// tier, which is the objectui#4000 ruling this pair encodes.
expect(ComponentRegistry.deprecationFor('div', 'html')).toBeUndefined();

// The bare and namespaced spellings answer alike, because a corpus authors
// whichever it likes and the gate must not have to know which.
expect(ComponentRegistry.deprecationFor('ui:div', 'json')).toBeDefined();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -158,4 +159,25 @@ describe('span deprecation notice — scoped by provenance (#4917)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED. Level with the sibling case in
* `div-deprecation-provenance.test.tsx`, for the reason objectui#4917 gave
* for bringing this renderer level in the first place: the two carry the same
* ruling, and a fact stated for one of them and not the other is how they
* diverge.
*
* The runtime exemption above and the declaration below are the same fact.
* Moving either alone turns this red.
*/
it('DECLARES the scope those cases demonstrate — deprecated on json, not on html', () => {
expect(ComponentRegistry.deprecationFor('span', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
});

expect(ComponentRegistry.deprecationFor('span', 'html')).toBeUndefined();
expect(ComponentRegistry.deprecationFor('ui:span', 'json')).toBeDefined();
});
});
28 changes: 27 additions & 1 deletion packages/components/src/renderers/basic/div.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,11 +103,37 @@ const DivRenderer = forwardRef<HTMLDivElement, { schema: DivSchema; className?:
}
);

ComponentRegistry.register('div',
ComponentRegistry.register('div',
DivRenderer,
{
namespace: 'ui',
label: 'Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674).
*
* Until this key existed, the only two statements that this type is
* deprecated were `DIV_DEPRECATION_NOTICE` — a string literal inside a
* renderer — and the word inside `label`. Neither can be consulted by a
* gate, a test or a type, which is why a deprecated type could be authored
* 85 times across 27 shipped exemplars with every check in the repository
* green: both gates that touch component types ask whether the type
* RESOLVES, and this one resolves.
*
* `surfaces` carries the objectui#4000 ruling rather than restating it in a
* second place: the `isHtmlTierNode` exemption ABOVE and this list are the
* same fact, and `__tests__/div-deprecation-provenance.test.tsx` pins them
* to each other so neither can move alone.
*
* ⛔ Declaring this deprecates NOTHING NEW and fails NO build. The catalog
* ratchet (`examples/schema-catalog/test/deprecated-component-types.test.ts`,
* objectui#6732) freezes the existing stock and refuses growth; draining it
* is objectui#3965's worklist.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
Expand Down
18 changes: 17 additions & 1 deletion packages/components/src/renderers/basic/span.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -146,11 +146,27 @@ const SpanRenderer = forwardRef<HTMLSpanElement, { schema: TextSpanSchema; class
}
);

ComponentRegistry.register('span',
ComponentRegistry.register('span',
SpanRenderer,
{
namespace: 'ui',
label: 'Inline Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674),
* level with the sibling `div` registration for the same reason
* objectui#4917 brought this renderer level with it: `SPAN_DEPRECATION_NOTICE`
* is a string literal and `label` is prose, and no gate can read either.
*
* `surfaces` carries the objectui#4000 ruling — deprecated on the JSON
* authoring surface, permanent vocabulary of the `kind:'html'` tier — as the
* same fact the `isHtmlTierNode` exemption above applies at runtime.
* `__tests__/span-deprecation-provenance.test.tsx` pins the two together.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
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('^' + ".*" + ' feat(core): declare component deprecation in registry metadata, readable by a gate by claude[bot] · Pull Request #6822 · 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
25 changes: 25 additions & 0 deletions .changeset/6674-registry-deprecation-declaration.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
---
'@object-ui/components': minor
'@object-ui/core': minor
---

Component deprecation is now DECLARED, not just warned about (objectui#6674).

A deprecated component type used to be stated in exactly two places, neither of
which a gate, a test or a type can consult: a `console.warn` string literal
inside the renderer, and the word "(Deprecated)" inside a human-readable
`label`. Both gates that touch component types ask a different question —
whether the type RESOLVES — and a deprecated type resolves, which is how one
could be authored 85 times across 27 shipped exemplars with every check green.

- `@object-ui/core` gains `ComponentDeprecation` / `AuthoringSurface` and the
`deprecated` key on the registration metadata, plus
`ComponentRegistry.deprecationFor(type, surface)` to read it back. The
declaration carries the SURFACES it applies to rather than being a boolean:
`div` and `span` are deprecated on the JSON authoring surface and are at the
same time permanent vocabulary of the `kind:'html'` tier, so a bare flag would
be false for one of its two readers.
- `@object-ui/components` marks `div` and `span` with the declaration their
console notices already state. Nothing new is deprecated and no build starts
failing: the catalog ratchet keeps the existing stock frozen, and draining it
stays objectui#3965's worklist.
127 changes: 100 additions & 27 deletions examples/schema-catalog/test/deprecated-component-types.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,14 +28,33 @@
* and walks `content/docs` and nothing else (`DOCS_ROOT = 'content/docs'`),
* so `examples/**` is outside its scan surface entirely.
*
* It is worse than the usual declared-but-unenforced shape: the deprecation is
* not declared anywhere MACHINE-READABLE. `RegistryComponentMetaExtras` carries
* `tier` / `namespace` / `skipFallback` / `labelAssociation` and has no
* `deprecated` field, so the only statements of it are a `console.warn` string
* literal in `div.tsx` / `span.tsx` and the human-readable label
* `'Container (Deprecated)'`. `DEPRECATED_TYPES` below is therefore a hand-kept
* mirror, and `the deprecation this ratchet mirrors is still declared` is the
* arm that stops the mirror from outliving the thing it mirrors.
* It was worse than the usual declared-but-unenforced shape: when this file
* landed, the deprecation was not declared anywhere MACHINE-READABLE.
* `RegistryComponentMetaExtras` carried `tier` / `namespace` / `skipFallback` /
* `labelling` and no `deprecated` field, so the only statements of it were a
* `console.warn` string literal in `div.tsx` / `span.tsx` and the
* human-readable label `'Container (Deprecated)'`. This file was therefore
* built on a hand-kept mirror whose premise arm READ THE RENDERER'S SOURCE and
* regex-matched that console literal — the closest thing to asking "is this
* type deprecated?" that existed.
*
* ## What objectui#6674 changed, and what it did not
*
* The registration now DECLARES it: `deprecated: { surfaces: ['json'],
* replacement: … }`, read back through `ComponentRegistry.deprecationFor(type,
* surface)`. So `the deprecation this ratchet mirrors is still declared` asks
* the registry instead of grepping a `.tsx` for a console string, and
* `no LOADED registration declares a deprecation this list omits` is the new
* arm that direction makes possible at all.
*
* ⚠️ `DEPRECATED_TYPES` stays HAND-KEPT on purpose, and deriving it wholesale
* from the registry would be a regression rather than the obvious next step.
* This file loads `@object-ui/components` and nothing else; a type declared
* deprecated by a plugin package it does not import would silently drop out of
* a derived list, and the census would shrink to green. The list is the
* ratchet's authority precisely because it is complete by construction. What
* the declaration buys is that the list can now be CHECKED — in both directions
* — against something a machine can read.
*
* ## Why this ratchet freezes the stock instead of demanding zero
*
Expand DownExpand Up@@ -114,17 +133,30 @@
import { describe, it, expect } from 'vitest';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
// Registers `div` / `span` (and the rest of the basic set) at module scope, so
// the two arms below can ASK the registry what is deprecated instead of reading
// a renderer's source. Module scope, not a hook — objectui#3010/#3021.
import '@object-ui/components';
import { ComponentRegistry } from '@object-ui/core';

/** Resolved off this module, so the gate does not depend on the process cwd. */
const SCHEMAS_ROOT = fileURLToPath(new URL('../src/schemas', import.meta.url));
const COMPONENTS_SRC = fileURLToPath(
new URL('../../../packages/components/src/renderers/basic', import.meta.url),
);

/**
* The deprecated JSON-authored component types, mirrored by hand from the
* renderers' notices because the registry carries no `deprecated` flag. Kept
* honest by `the deprecation this ratchet mirrors is still declared`.
* The surface this corpus is authored on. Every fixture under `SCHEMAS_ROOT` is
* JSON metadata, so the question this ratchet asks the registry is scoped to
* it: `div` and `span` are ALSO permanent vocabulary of the `kind:'html'` tier
* (objectui#4000), where the parser compiles the plain tag straight through and
* no other spelling exists to migrate to. A gate that dropped the scope would
* be refusing a spelling that is correct on the other surface.
*/
const CORPUS_SURFACE = 'json' as const;

/**
* The deprecated JSON-authored component types this ratchet refuses. Hand-kept
* — see the header for why deriving it from the registry would shrink the
* census silently — and now checked in BOTH directions against the
* machine-readable declaration the registrations carry (objectui#6674).
*/
const DEPRECATED_TYPES = ['div', 'span'] as const;

Expand DownExpand Up@@ -293,19 +325,60 @@ describe('deprecated component types in the catalog are ratcheted (#3965)', () =

it('the deprecation this ratchet mirrors is still declared', () => {
// The mirror's premise. If a type is UN-deprecated, this file must die
// loudly rather than keep refusing a spelling that became legal again;
// `DEPRECATED_TYPES` is hand-kept precisely because the registry carries no
// machine-readable flag to derive it from.
for (const type of DEPRECATED_TYPES) {
const source = readFileSync(`${COMPONENTS_SRC}/${type}.tsx`, 'utf8');
expect(
source,
`renderers/basic/${type}.tsx no longer declares its deprecation notice. ` +
`Either the type was un-deprecated — in which case drop it from ` +
`DEPRECATED_TYPES and retire the matching baseline — or the notice ` +
`moved and this mirror needs re-pointing.`,
).toContain(`The "${type}" component is deprecated`);
}
// loudly rather than keep refusing a spelling that became legal again.
//
// This arm used to `readFileSync` the renderer and regex-match its
// `console.warn` literal, because that string was one of only two places a
// deprecation was stated and the only one a test could reach. It now asks
// the registry, which is objectui#6674's whole delivery: the question "is
// this type deprecated?" has an asker.
const undeclared = DEPRECATED_TYPES.filter(
(type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE) === undefined,
);

expect(
undeclared,
'A type this ratchet refuses no longer DECLARES a deprecation for the ' +
'json authoring surface. Either it was un-deprecated — in which case ' +
'drop it from DEPRECATED_TYPES and retire the matching baseline — or ' +
'the declaration moved and this mirror needs re-pointing. (A type ' +
'whose `surfaces` no longer lists `json` reads as un-deprecated HERE ' +
'and is still deprecated elsewhere; that is the objectui#4000 scope ' +
'working, not a bug in this arm.)',
).toEqual([]);
});

it('no LOADED registration declares a deprecation this list omits', () => {
// The direction the hand-kept mirror could never check. Before the
// declaration existed there was nothing to enumerate: a third deprecated
// type could have been added to `@object-ui/components` with a console
// string and a label, and this file would have gone on refusing exactly two.
//
// Scoped honestly to what this file LOADS — `@object-ui/components`. A
// plugin package's declaration is out of range here, which is the reason
// DEPRECATED_TYPES stays the authority rather than being derived.
//
// Non-vacuity is the arm ABOVE: an empty result here would also be produced
// by a registry that answered `undefined` for everything, and that state
// turns the premise arm red first. The two hold each other up.
const listed = new Set<string>(DEPRECATED_TYPES);
// A namespaced registration answers under BOTH spellings (`ui:div` and
// `div`); the corpus authors the bare one and the baseline is keyed on it.
// Either spelling being listed counts, and the raw key is what gets
// reported so the message names something that exists in the registry.
const bare = (key: string) => (key.includes(':') ? key.slice(key.indexOf(':') + 1) : key);
const missing = ComponentRegistry.getKnownTypes()
.filter((type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE))
.filter((type) => !listed.has(type) && !listed.has(bare(type)))
.sort();

expect(
missing,
'A loaded registration declares a json-surface deprecation that this ' +
'ratchet does not refuse. Add it to DEPRECATED_TYPES — and if the ' +
'corpus already authors it, baseline the existing stock in the same PR ' +
'rather than leaving the type unguarded.',
).toEqual([]);
});

it('the stock is exactly what this card measured, and the exemption is not a hole', () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -131,4 +132,38 @@ describe('div deprecation notice — scoped by provenance (#4000)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED, so a gate can read what the four
* cases above can only demonstrate by rendering.
*
* Before the registration carried `deprecated`, the only statements that this
* type is deprecated were the notice string literal and the word inside
* `label`; no gate, test or type could consult either, so both gates that
* touch component types ask whether the type RESOLVES instead — and it does.
*
* This case is the join. Above, the renderer EXEMPTS html-tier nodes at
* runtime; here, the registration DECLARES the identical scope. Asserting
* them in one file is what stops them drifting: widening `surfaces` to
* `['json', 'html']` without touching the exemption, or dropping the
* exemption without narrowing `surfaces`, turns this red — which no
* assertion about either one alone can do.
*/
it('DECLARES the scope those four cases demonstrate — deprecated on json, not on html', () => {
// Deprecated where the notice is fired…
expect(ComponentRegistry.deprecationFor('div', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
});

// …and NOT where the first case above proves the renderer stays silent. A
// declaration that said "deprecated" full stop would be false for the html
// tier, which is the objectui#4000 ruling this pair encodes.
expect(ComponentRegistry.deprecationFor('div', 'html')).toBeUndefined();

// The bare and namespaced spellings answer alike, because a corpus authors
// whichever it likes and the gate must not have to know which.
expect(ComponentRegistry.deprecationFor('ui:div', 'json')).toBeDefined();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -158,4 +159,25 @@ describe('span deprecation notice — scoped by provenance (#4917)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED. Level with the sibling case in
* `div-deprecation-provenance.test.tsx`, for the reason objectui#4917 gave
* for bringing this renderer level in the first place: the two carry the same
* ruling, and a fact stated for one of them and not the other is how they
* diverge.
*
* The runtime exemption above and the declaration below are the same fact.
* Moving either alone turns this red.
*/
it('DECLARES the scope those cases demonstrate — deprecated on json, not on html', () => {
expect(ComponentRegistry.deprecationFor('span', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
});

expect(ComponentRegistry.deprecationFor('span', 'html')).toBeUndefined();
expect(ComponentRegistry.deprecationFor('ui:span', 'json')).toBeDefined();
});
});
28 changes: 27 additions & 1 deletion packages/components/src/renderers/basic/div.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,11 +103,37 @@ const DivRenderer = forwardRef<HTMLDivElement, { schema: DivSchema; className?:
}
);

ComponentRegistry.register('div',
ComponentRegistry.register('div',
DivRenderer,
{
namespace: 'ui',
label: 'Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674).
*
* Until this key existed, the only two statements that this type is
* deprecated were `DIV_DEPRECATION_NOTICE` — a string literal inside a
* renderer — and the word inside `label`. Neither can be consulted by a
* gate, a test or a type, which is why a deprecated type could be authored
* 85 times across 27 shipped exemplars with every check in the repository
* green: both gates that touch component types ask whether the type
* RESOLVES, and this one resolves.
*
* `surfaces` carries the objectui#4000 ruling rather than restating it in a
* second place: the `isHtmlTierNode` exemption ABOVE and this list are the
* same fact, and `__tests__/div-deprecation-provenance.test.tsx` pins them
* to each other so neither can move alone.
*
* ⛔ Declaring this deprecates NOTHING NEW and fails NO build. The catalog
* ratchet (`examples/schema-catalog/test/deprecated-component-types.test.ts`,
* objectui#6732) freezes the existing stock and refuses growth; draining it
* is objectui#3965's worklist.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
Expand Down
18 changes: 17 additions & 1 deletion packages/components/src/renderers/basic/span.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -146,11 +146,27 @@ const SpanRenderer = forwardRef<HTMLSpanElement, { schema: TextSpanSchema; class
}
);

ComponentRegistry.register('span',
ComponentRegistry.register('span',
SpanRenderer,
{
namespace: 'ui',
label: 'Inline Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674),
* level with the sibling `div` registration for the same reason
* objectui#4917 brought this renderer level with it: `SPAN_DEPRECATION_NOTICE`
* is a string literal and `label` is prose, and no gate can read either.
*
* `surfaces` carries the objectui#4000 ruling — deprecated on the JSON
* authoring surface, permanent vocabulary of the `kind:'html'` tier — as the
* same fact the `isHtmlTierNode` exemption above applies at runtime.
* `__tests__/span-deprecation-provenance.test.tsx` pins the two together.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
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('^' + ".*" + ' feat(core): declare component deprecation in registry metadata, readable by a gate by claude[bot] · Pull Request #6822 · 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
25 changes: 25 additions & 0 deletions .changeset/6674-registry-deprecation-declaration.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
---
'@object-ui/components': minor
'@object-ui/core': minor
---

Component deprecation is now DECLARED, not just warned about (objectui#6674).

A deprecated component type used to be stated in exactly two places, neither of
which a gate, a test or a type can consult: a `console.warn` string literal
inside the renderer, and the word "(Deprecated)" inside a human-readable
`label`. Both gates that touch component types ask a different question —
whether the type RESOLVES — and a deprecated type resolves, which is how one
could be authored 85 times across 27 shipped exemplars with every check green.

- `@object-ui/core` gains `ComponentDeprecation` / `AuthoringSurface` and the
`deprecated` key on the registration metadata, plus
`ComponentRegistry.deprecationFor(type, surface)` to read it back. The
declaration carries the SURFACES it applies to rather than being a boolean:
`div` and `span` are deprecated on the JSON authoring surface and are at the
same time permanent vocabulary of the `kind:'html'` tier, so a bare flag would
be false for one of its two readers.
- `@object-ui/components` marks `div` and `span` with the declaration their
console notices already state. Nothing new is deprecated and no build starts
failing: the catalog ratchet keeps the existing stock frozen, and draining it
stays objectui#3965's worklist.
127 changes: 100 additions & 27 deletions examples/schema-catalog/test/deprecated-component-types.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,14 +28,33 @@
* and walks `content/docs` and nothing else (`DOCS_ROOT = 'content/docs'`),
* so `examples/**` is outside its scan surface entirely.
*
* It is worse than the usual declared-but-unenforced shape: the deprecation is
* not declared anywhere MACHINE-READABLE. `RegistryComponentMetaExtras` carries
* `tier` / `namespace` / `skipFallback` / `labelAssociation` and has no
* `deprecated` field, so the only statements of it are a `console.warn` string
* literal in `div.tsx` / `span.tsx` and the human-readable label
* `'Container (Deprecated)'`. `DEPRECATED_TYPES` below is therefore a hand-kept
* mirror, and `the deprecation this ratchet mirrors is still declared` is the
* arm that stops the mirror from outliving the thing it mirrors.
* It was worse than the usual declared-but-unenforced shape: when this file
* landed, the deprecation was not declared anywhere MACHINE-READABLE.
* `RegistryComponentMetaExtras` carried `tier` / `namespace` / `skipFallback` /
* `labelling` and no `deprecated` field, so the only statements of it were a
* `console.warn` string literal in `div.tsx` / `span.tsx` and the
* human-readable label `'Container (Deprecated)'`. This file was therefore
* built on a hand-kept mirror whose premise arm READ THE RENDERER'S SOURCE and
* regex-matched that console literal — the closest thing to asking "is this
* type deprecated?" that existed.
*
* ## What objectui#6674 changed, and what it did not
*
* The registration now DECLARES it: `deprecated: { surfaces: ['json'],
* replacement: … }`, read back through `ComponentRegistry.deprecationFor(type,
* surface)`. So `the deprecation this ratchet mirrors is still declared` asks
* the registry instead of grepping a `.tsx` for a console string, and
* `no LOADED registration declares a deprecation this list omits` is the new
* arm that direction makes possible at all.
*
* ⚠️ `DEPRECATED_TYPES` stays HAND-KEPT on purpose, and deriving it wholesale
* from the registry would be a regression rather than the obvious next step.
* This file loads `@object-ui/components` and nothing else; a type declared
* deprecated by a plugin package it does not import would silently drop out of
* a derived list, and the census would shrink to green. The list is the
* ratchet's authority precisely because it is complete by construction. What
* the declaration buys is that the list can now be CHECKED — in both directions
* — against something a machine can read.
*
* ## Why this ratchet freezes the stock instead of demanding zero
*
Expand DownExpand Up@@ -114,17 +133,30 @@
import { describe, it, expect } from 'vitest';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
// Registers `div` / `span` (and the rest of the basic set) at module scope, so
// the two arms below can ASK the registry what is deprecated instead of reading
// a renderer's source. Module scope, not a hook — objectui#3010/#3021.
import '@object-ui/components';
import { ComponentRegistry } from '@object-ui/core';

/** Resolved off this module, so the gate does not depend on the process cwd. */
const SCHEMAS_ROOT = fileURLToPath(new URL('../src/schemas', import.meta.url));
const COMPONENTS_SRC = fileURLToPath(
new URL('../../../packages/components/src/renderers/basic', import.meta.url),
);

/**
* The deprecated JSON-authored component types, mirrored by hand from the
* renderers' notices because the registry carries no `deprecated` flag. Kept
* honest by `the deprecation this ratchet mirrors is still declared`.
* The surface this corpus is authored on. Every fixture under `SCHEMAS_ROOT` is
* JSON metadata, so the question this ratchet asks the registry is scoped to
* it: `div` and `span` are ALSO permanent vocabulary of the `kind:'html'` tier
* (objectui#4000), where the parser compiles the plain tag straight through and
* no other spelling exists to migrate to. A gate that dropped the scope would
* be refusing a spelling that is correct on the other surface.
*/
const CORPUS_SURFACE = 'json' as const;

/**
* The deprecated JSON-authored component types this ratchet refuses. Hand-kept
* — see the header for why deriving it from the registry would shrink the
* census silently — and now checked in BOTH directions against the
* machine-readable declaration the registrations carry (objectui#6674).
*/
const DEPRECATED_TYPES = ['div', 'span'] as const;

Expand DownExpand Up@@ -293,19 +325,60 @@ describe('deprecated component types in the catalog are ratcheted (#3965)', () =

it('the deprecation this ratchet mirrors is still declared', () => {
// The mirror's premise. If a type is UN-deprecated, this file must die
// loudly rather than keep refusing a spelling that became legal again;
// `DEPRECATED_TYPES` is hand-kept precisely because the registry carries no
// machine-readable flag to derive it from.
for (const type of DEPRECATED_TYPES) {
const source = readFileSync(`${COMPONENTS_SRC}/${type}.tsx`, 'utf8');
expect(
source,
`renderers/basic/${type}.tsx no longer declares its deprecation notice. ` +
`Either the type was un-deprecated — in which case drop it from ` +
`DEPRECATED_TYPES and retire the matching baseline — or the notice ` +
`moved and this mirror needs re-pointing.`,
).toContain(`The "${type}" component is deprecated`);
}
// loudly rather than keep refusing a spelling that became legal again.
//
// This arm used to `readFileSync` the renderer and regex-match its
// `console.warn` literal, because that string was one of only two places a
// deprecation was stated and the only one a test could reach. It now asks
// the registry, which is objectui#6674's whole delivery: the question "is
// this type deprecated?" has an asker.
const undeclared = DEPRECATED_TYPES.filter(
(type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE) === undefined,
);

expect(
undeclared,
'A type this ratchet refuses no longer DECLARES a deprecation for the ' +
'json authoring surface. Either it was un-deprecated — in which case ' +
'drop it from DEPRECATED_TYPES and retire the matching baseline — or ' +
'the declaration moved and this mirror needs re-pointing. (A type ' +
'whose `surfaces` no longer lists `json` reads as un-deprecated HERE ' +
'and is still deprecated elsewhere; that is the objectui#4000 scope ' +
'working, not a bug in this arm.)',
).toEqual([]);
});

it('no LOADED registration declares a deprecation this list omits', () => {
// The direction the hand-kept mirror could never check. Before the
// declaration existed there was nothing to enumerate: a third deprecated
// type could have been added to `@object-ui/components` with a console
// string and a label, and this file would have gone on refusing exactly two.
//
// Scoped honestly to what this file LOADS — `@object-ui/components`. A
// plugin package's declaration is out of range here, which is the reason
// DEPRECATED_TYPES stays the authority rather than being derived.
//
// Non-vacuity is the arm ABOVE: an empty result here would also be produced
// by a registry that answered `undefined` for everything, and that state
// turns the premise arm red first. The two hold each other up.
const listed = new Set<string>(DEPRECATED_TYPES);
// A namespaced registration answers under BOTH spellings (`ui:div` and
// `div`); the corpus authors the bare one and the baseline is keyed on it.
// Either spelling being listed counts, and the raw key is what gets
// reported so the message names something that exists in the registry.
const bare = (key: string) => (key.includes(':') ? key.slice(key.indexOf(':') + 1) : key);
const missing = ComponentRegistry.getKnownTypes()
.filter((type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE))
.filter((type) => !listed.has(type) && !listed.has(bare(type)))
.sort();

expect(
missing,
'A loaded registration declares a json-surface deprecation that this ' +
'ratchet does not refuse. Add it to DEPRECATED_TYPES — and if the ' +
'corpus already authors it, baseline the existing stock in the same PR ' +
'rather than leaving the type unguarded.',
).toEqual([]);
});

it('the stock is exactly what this card measured, and the exemption is not a hole', () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -131,4 +132,38 @@ describe('div deprecation notice — scoped by provenance (#4000)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED, so a gate can read what the four
* cases above can only demonstrate by rendering.
*
* Before the registration carried `deprecated`, the only statements that this
* type is deprecated were the notice string literal and the word inside
* `label`; no gate, test or type could consult either, so both gates that
* touch component types ask whether the type RESOLVES instead — and it does.
*
* This case is the join. Above, the renderer EXEMPTS html-tier nodes at
* runtime; here, the registration DECLARES the identical scope. Asserting
* them in one file is what stops them drifting: widening `surfaces` to
* `['json', 'html']` without touching the exemption, or dropping the
* exemption without narrowing `surfaces`, turns this red — which no
* assertion about either one alone can do.
*/
it('DECLARES the scope those four cases demonstrate — deprecated on json, not on html', () => {
// Deprecated where the notice is fired…
expect(ComponentRegistry.deprecationFor('div', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
});

// …and NOT where the first case above proves the renderer stays silent. A
// declaration that said "deprecated" full stop would be false for the html
// tier, which is the objectui#4000 ruling this pair encodes.
expect(ComponentRegistry.deprecationFor('div', 'html')).toBeUndefined();

// The bare and namespaced spellings answer alike, because a corpus authors
// whichever it likes and the gate must not have to know which.
expect(ComponentRegistry.deprecationFor('ui:div', 'json')).toBeDefined();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -158,4 +159,25 @@ describe('span deprecation notice — scoped by provenance (#4917)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED. Level with the sibling case in
* `div-deprecation-provenance.test.tsx`, for the reason objectui#4917 gave
* for bringing this renderer level in the first place: the two carry the same
* ruling, and a fact stated for one of them and not the other is how they
* diverge.
*
* The runtime exemption above and the declaration below are the same fact.
* Moving either alone turns this red.
*/
it('DECLARES the scope those cases demonstrate — deprecated on json, not on html', () => {
expect(ComponentRegistry.deprecationFor('span', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
});

expect(ComponentRegistry.deprecationFor('span', 'html')).toBeUndefined();
expect(ComponentRegistry.deprecationFor('ui:span', 'json')).toBeDefined();
});
});
28 changes: 27 additions & 1 deletion packages/components/src/renderers/basic/div.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,11 +103,37 @@ const DivRenderer = forwardRef<HTMLDivElement, { schema: DivSchema; className?:
}
);

ComponentRegistry.register('div',
ComponentRegistry.register('div',
DivRenderer,
{
namespace: 'ui',
label: 'Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674).
*
* Until this key existed, the only two statements that this type is
* deprecated were `DIV_DEPRECATION_NOTICE` — a string literal inside a
* renderer — and the word inside `label`. Neither can be consulted by a
* gate, a test or a type, which is why a deprecated type could be authored
* 85 times across 27 shipped exemplars with every check in the repository
* green: both gates that touch component types ask whether the type
* RESOLVES, and this one resolves.
*
* `surfaces` carries the objectui#4000 ruling rather than restating it in a
* second place: the `isHtmlTierNode` exemption ABOVE and this list are the
* same fact, and `__tests__/div-deprecation-provenance.test.tsx` pins them
* to each other so neither can move alone.
*
* ⛔ Declaring this deprecates NOTHING NEW and fails NO build. The catalog
* ratchet (`examples/schema-catalog/test/deprecated-component-types.test.ts`,
* objectui#6732) freezes the existing stock and refuses growth; draining it
* is objectui#3965's worklist.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
Expand Down
18 changes: 17 additions & 1 deletion packages/components/src/renderers/basic/span.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -146,11 +146,27 @@ const SpanRenderer = forwardRef<HTMLSpanElement, { schema: TextSpanSchema; class
}
);

ComponentRegistry.register('span',
ComponentRegistry.register('span',
SpanRenderer,
{
namespace: 'ui',
label: 'Inline Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674),
* level with the sibling `div` registration for the same reason
* objectui#4917 brought this renderer level with it: `SPAN_DEPRECATION_NOTICE`
* is a string literal and `label` is prose, and no gate can read either.
*
* `surfaces` carries the objectui#4000 ruling — deprecated on the JSON
* authoring surface, permanent vocabulary of the `kind:'html'` tier — as the
* same fact the `isHtmlTierNode` exemption above applies at runtime.
* `__tests__/span-deprecation-provenance.test.tsx` pins the two together.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
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" + ' feat(core): declare component deprecation in registry metadata, readable by a gate by claude[bot] · Pull Request #6822 · 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
25 changes: 25 additions & 0 deletions .changeset/6674-registry-deprecation-declaration.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
---
'@object-ui/components': minor
'@object-ui/core': minor
---

Component deprecation is now DECLARED, not just warned about (objectui#6674).

A deprecated component type used to be stated in exactly two places, neither of
which a gate, a test or a type can consult: a `console.warn` string literal
inside the renderer, and the word "(Deprecated)" inside a human-readable
`label`. Both gates that touch component types ask a different question —
whether the type RESOLVES — and a deprecated type resolves, which is how one
could be authored 85 times across 27 shipped exemplars with every check green.

- `@object-ui/core` gains `ComponentDeprecation` / `AuthoringSurface` and the
`deprecated` key on the registration metadata, plus
`ComponentRegistry.deprecationFor(type, surface)` to read it back. The
declaration carries the SURFACES it applies to rather than being a boolean:
`div` and `span` are deprecated on the JSON authoring surface and are at the
same time permanent vocabulary of the `kind:'html'` tier, so a bare flag would
be false for one of its two readers.
- `@object-ui/components` marks `div` and `span` with the declaration their
console notices already state. Nothing new is deprecated and no build starts
failing: the catalog ratchet keeps the existing stock frozen, and draining it
stays objectui#3965's worklist.
127 changes: 100 additions & 27 deletions examples/schema-catalog/test/deprecated-component-types.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,14 +28,33 @@
* and walks `content/docs` and nothing else (`DOCS_ROOT = 'content/docs'`),
* so `examples/**` is outside its scan surface entirely.
*
* It is worse than the usual declared-but-unenforced shape: the deprecation is
* not declared anywhere MACHINE-READABLE. `RegistryComponentMetaExtras` carries
* `tier` / `namespace` / `skipFallback` / `labelAssociation` and has no
* `deprecated` field, so the only statements of it are a `console.warn` string
* literal in `div.tsx` / `span.tsx` and the human-readable label
* `'Container (Deprecated)'`. `DEPRECATED_TYPES` below is therefore a hand-kept
* mirror, and `the deprecation this ratchet mirrors is still declared` is the
* arm that stops the mirror from outliving the thing it mirrors.
* It was worse than the usual declared-but-unenforced shape: when this file
* landed, the deprecation was not declared anywhere MACHINE-READABLE.
* `RegistryComponentMetaExtras` carried `tier` / `namespace` / `skipFallback` /
* `labelling` and no `deprecated` field, so the only statements of it were a
* `console.warn` string literal in `div.tsx` / `span.tsx` and the
* human-readable label `'Container (Deprecated)'`. This file was therefore
* built on a hand-kept mirror whose premise arm READ THE RENDERER'S SOURCE and
* regex-matched that console literal — the closest thing to asking "is this
* type deprecated?" that existed.
*
* ## What objectui#6674 changed, and what it did not
*
* The registration now DECLARES it: `deprecated: { surfaces: ['json'],
* replacement: … }`, read back through `ComponentRegistry.deprecationFor(type,
* surface)`. So `the deprecation this ratchet mirrors is still declared` asks
* the registry instead of grepping a `.tsx` for a console string, and
* `no LOADED registration declares a deprecation this list omits` is the new
* arm that direction makes possible at all.
*
* ⚠️ `DEPRECATED_TYPES` stays HAND-KEPT on purpose, and deriving it wholesale
* from the registry would be a regression rather than the obvious next step.
* This file loads `@object-ui/components` and nothing else; a type declared
* deprecated by a plugin package it does not import would silently drop out of
* a derived list, and the census would shrink to green. The list is the
* ratchet's authority precisely because it is complete by construction. What
* the declaration buys is that the list can now be CHECKED — in both directions
* — against something a machine can read.
*
* ## Why this ratchet freezes the stock instead of demanding zero
*
Expand DownExpand Up@@ -114,17 +133,30 @@
import { describe, it, expect } from 'vitest';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
// Registers `div` / `span` (and the rest of the basic set) at module scope, so
// the two arms below can ASK the registry what is deprecated instead of reading
// a renderer's source. Module scope, not a hook — objectui#3010/#3021.
import '@object-ui/components';
import { ComponentRegistry } from '@object-ui/core';

/** Resolved off this module, so the gate does not depend on the process cwd. */
const SCHEMAS_ROOT = fileURLToPath(new URL('../src/schemas', import.meta.url));
const COMPONENTS_SRC = fileURLToPath(
new URL('../../../packages/components/src/renderers/basic', import.meta.url),
);

/**
* The deprecated JSON-authored component types, mirrored by hand from the
* renderers' notices because the registry carries no `deprecated` flag. Kept
* honest by `the deprecation this ratchet mirrors is still declared`.
* The surface this corpus is authored on. Every fixture under `SCHEMAS_ROOT` is
* JSON metadata, so the question this ratchet asks the registry is scoped to
* it: `div` and `span` are ALSO permanent vocabulary of the `kind:'html'` tier
* (objectui#4000), where the parser compiles the plain tag straight through and
* no other spelling exists to migrate to. A gate that dropped the scope would
* be refusing a spelling that is correct on the other surface.
*/
const CORPUS_SURFACE = 'json' as const;

/**
* The deprecated JSON-authored component types this ratchet refuses. Hand-kept
* — see the header for why deriving it from the registry would shrink the
* census silently — and now checked in BOTH directions against the
* machine-readable declaration the registrations carry (objectui#6674).
*/
const DEPRECATED_TYPES = ['div', 'span'] as const;

Expand DownExpand Up@@ -293,19 +325,60 @@ describe('deprecated component types in the catalog are ratcheted (#3965)', () =

it('the deprecation this ratchet mirrors is still declared', () => {
// The mirror's premise. If a type is UN-deprecated, this file must die
// loudly rather than keep refusing a spelling that became legal again;
// `DEPRECATED_TYPES` is hand-kept precisely because the registry carries no
// machine-readable flag to derive it from.
for (const type of DEPRECATED_TYPES) {
const source = readFileSync(`${COMPONENTS_SRC}/${type}.tsx`, 'utf8');
expect(
source,
`renderers/basic/${type}.tsx no longer declares its deprecation notice. ` +
`Either the type was un-deprecated — in which case drop it from ` +
`DEPRECATED_TYPES and retire the matching baseline — or the notice ` +
`moved and this mirror needs re-pointing.`,
).toContain(`The "${type}" component is deprecated`);
}
// loudly rather than keep refusing a spelling that became legal again.
//
// This arm used to `readFileSync` the renderer and regex-match its
// `console.warn` literal, because that string was one of only two places a
// deprecation was stated and the only one a test could reach. It now asks
// the registry, which is objectui#6674's whole delivery: the question "is
// this type deprecated?" has an asker.
const undeclared = DEPRECATED_TYPES.filter(
(type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE) === undefined,
);

expect(
undeclared,
'A type this ratchet refuses no longer DECLARES a deprecation for the ' +
'json authoring surface. Either it was un-deprecated — in which case ' +
'drop it from DEPRECATED_TYPES and retire the matching baseline — or ' +
'the declaration moved and this mirror needs re-pointing. (A type ' +
'whose `surfaces` no longer lists `json` reads as un-deprecated HERE ' +
'and is still deprecated elsewhere; that is the objectui#4000 scope ' +
'working, not a bug in this arm.)',
).toEqual([]);
});

it('no LOADED registration declares a deprecation this list omits', () => {
// The direction the hand-kept mirror could never check. Before the
// declaration existed there was nothing to enumerate: a third deprecated
// type could have been added to `@object-ui/components` with a console
// string and a label, and this file would have gone on refusing exactly two.
//
// Scoped honestly to what this file LOADS — `@object-ui/components`. A
// plugin package's declaration is out of range here, which is the reason
// DEPRECATED_TYPES stays the authority rather than being derived.
//
// Non-vacuity is the arm ABOVE: an empty result here would also be produced
// by a registry that answered `undefined` for everything, and that state
// turns the premise arm red first. The two hold each other up.
const listed = new Set<string>(DEPRECATED_TYPES);
// A namespaced registration answers under BOTH spellings (`ui:div` and
// `div`); the corpus authors the bare one and the baseline is keyed on it.
// Either spelling being listed counts, and the raw key is what gets
// reported so the message names something that exists in the registry.
const bare = (key: string) => (key.includes(':') ? key.slice(key.indexOf(':') + 1) : key);
const missing = ComponentRegistry.getKnownTypes()
.filter((type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE))
.filter((type) => !listed.has(type) && !listed.has(bare(type)))
.sort();

expect(
missing,
'A loaded registration declares a json-surface deprecation that this ' +
'ratchet does not refuse. Add it to DEPRECATED_TYPES — and if the ' +
'corpus already authors it, baseline the existing stock in the same PR ' +
'rather than leaving the type unguarded.',
).toEqual([]);
});

it('the stock is exactly what this card measured, and the exemption is not a hole', () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -131,4 +132,38 @@ describe('div deprecation notice — scoped by provenance (#4000)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED, so a gate can read what the four
* cases above can only demonstrate by rendering.
*
* Before the registration carried `deprecated`, the only statements that this
* type is deprecated were the notice string literal and the word inside
* `label`; no gate, test or type could consult either, so both gates that
* touch component types ask whether the type RESOLVES instead — and it does.
*
* This case is the join. Above, the renderer EXEMPTS html-tier nodes at
* runtime; here, the registration DECLARES the identical scope. Asserting
* them in one file is what stops them drifting: widening `surfaces` to
* `['json', 'html']` without touching the exemption, or dropping the
* exemption without narrowing `surfaces`, turns this red — which no
* assertion about either one alone can do.
*/
it('DECLARES the scope those four cases demonstrate — deprecated on json, not on html', () => {
// Deprecated where the notice is fired…
expect(ComponentRegistry.deprecationFor('div', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
});

// …and NOT where the first case above proves the renderer stays silent. A
// declaration that said "deprecated" full stop would be false for the html
// tier, which is the objectui#4000 ruling this pair encodes.
expect(ComponentRegistry.deprecationFor('div', 'html')).toBeUndefined();

// The bare and namespaced spellings answer alike, because a corpus authors
// whichever it likes and the gate must not have to know which.
expect(ComponentRegistry.deprecationFor('ui:div', 'json')).toBeDefined();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -158,4 +159,25 @@ describe('span deprecation notice — scoped by provenance (#4917)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED. Level with the sibling case in
* `div-deprecation-provenance.test.tsx`, for the reason objectui#4917 gave
* for bringing this renderer level in the first place: the two carry the same
* ruling, and a fact stated for one of them and not the other is how they
* diverge.
*
* The runtime exemption above and the declaration below are the same fact.
* Moving either alone turns this red.
*/
it('DECLARES the scope those cases demonstrate — deprecated on json, not on html', () => {
expect(ComponentRegistry.deprecationFor('span', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
});

expect(ComponentRegistry.deprecationFor('span', 'html')).toBeUndefined();
expect(ComponentRegistry.deprecationFor('ui:span', 'json')).toBeDefined();
});
});
28 changes: 27 additions & 1 deletion packages/components/src/renderers/basic/div.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,11 +103,37 @@ const DivRenderer = forwardRef<HTMLDivElement, { schema: DivSchema; className?:
}
);

ComponentRegistry.register('div',
ComponentRegistry.register('div',
DivRenderer,
{
namespace: 'ui',
label: 'Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674).
*
* Until this key existed, the only two statements that this type is
* deprecated were `DIV_DEPRECATION_NOTICE` — a string literal inside a
* renderer — and the word inside `label`. Neither can be consulted by a
* gate, a test or a type, which is why a deprecated type could be authored
* 85 times across 27 shipped exemplars with every check in the repository
* green: both gates that touch component types ask whether the type
* RESOLVES, and this one resolves.
*
* `surfaces` carries the objectui#4000 ruling rather than restating it in a
* second place: the `isHtmlTierNode` exemption ABOVE and this list are the
* same fact, and `__tests__/div-deprecation-provenance.test.tsx` pins them
* to each other so neither can move alone.
*
* ⛔ Declaring this deprecates NOTHING NEW and fails NO build. The catalog
* ratchet (`examples/schema-catalog/test/deprecated-component-types.test.ts`,
* objectui#6732) freezes the existing stock and refuses growth; draining it
* is objectui#3965's worklist.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
Expand Down
18 changes: 17 additions & 1 deletion packages/components/src/renderers/basic/span.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -146,11 +146,27 @@ const SpanRenderer = forwardRef<HTMLSpanElement, { schema: TextSpanSchema; class
}
);

ComponentRegistry.register('span',
ComponentRegistry.register('span',
SpanRenderer,
{
namespace: 'ui',
label: 'Inline Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674),
* level with the sibling `div` registration for the same reason
* objectui#4917 brought this renderer level with it: `SPAN_DEPRECATION_NOTICE`
* is a string literal and `label` is prose, and no gate can read either.
*
* `surfaces` carries the objectui#4000 ruling — deprecated on the JSON
* authoring surface, permanent vocabulary of the `kind:'html'` tier — as the
* same fact the `isHtmlTierNode` exemption above applies at runtime.
* `__tests__/span-deprecation-provenance.test.tsx` pins the two together.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
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('^' + ".*" + ' feat(core): declare component deprecation in registry metadata, readable by a gate by claude[bot] · Pull Request #6822 · 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
25 changes: 25 additions & 0 deletions .changeset/6674-registry-deprecation-declaration.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
---
'@object-ui/components': minor
'@object-ui/core': minor
---

Component deprecation is now DECLARED, not just warned about (objectui#6674).

A deprecated component type used to be stated in exactly two places, neither of
which a gate, a test or a type can consult: a `console.warn` string literal
inside the renderer, and the word "(Deprecated)" inside a human-readable
`label`. Both gates that touch component types ask a different question —
whether the type RESOLVES — and a deprecated type resolves, which is how one
could be authored 85 times across 27 shipped exemplars with every check green.

- `@object-ui/core` gains `ComponentDeprecation` / `AuthoringSurface` and the
`deprecated` key on the registration metadata, plus
`ComponentRegistry.deprecationFor(type, surface)` to read it back. The
declaration carries the SURFACES it applies to rather than being a boolean:
`div` and `span` are deprecated on the JSON authoring surface and are at the
same time permanent vocabulary of the `kind:'html'` tier, so a bare flag would
be false for one of its two readers.
- `@object-ui/components` marks `div` and `span` with the declaration their
console notices already state. Nothing new is deprecated and no build starts
failing: the catalog ratchet keeps the existing stock frozen, and draining it
stays objectui#3965's worklist.
127 changes: 100 additions & 27 deletions examples/schema-catalog/test/deprecated-component-types.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,14 +28,33 @@
* and walks `content/docs` and nothing else (`DOCS_ROOT = 'content/docs'`),
* so `examples/**` is outside its scan surface entirely.
*
* It is worse than the usual declared-but-unenforced shape: the deprecation is
* not declared anywhere MACHINE-READABLE. `RegistryComponentMetaExtras` carries
* `tier` / `namespace` / `skipFallback` / `labelAssociation` and has no
* `deprecated` field, so the only statements of it are a `console.warn` string
* literal in `div.tsx` / `span.tsx` and the human-readable label
* `'Container (Deprecated)'`. `DEPRECATED_TYPES` below is therefore a hand-kept
* mirror, and `the deprecation this ratchet mirrors is still declared` is the
* arm that stops the mirror from outliving the thing it mirrors.
* It was worse than the usual declared-but-unenforced shape: when this file
* landed, the deprecation was not declared anywhere MACHINE-READABLE.
* `RegistryComponentMetaExtras` carried `tier` / `namespace` / `skipFallback` /
* `labelling` and no `deprecated` field, so the only statements of it were a
* `console.warn` string literal in `div.tsx` / `span.tsx` and the
* human-readable label `'Container (Deprecated)'`. This file was therefore
* built on a hand-kept mirror whose premise arm READ THE RENDERER'S SOURCE and
* regex-matched that console literal — the closest thing to asking "is this
* type deprecated?" that existed.
*
* ## What objectui#6674 changed, and what it did not
*
* The registration now DECLARES it: `deprecated: { surfaces: ['json'],
* replacement: … }`, read back through `ComponentRegistry.deprecationFor(type,
* surface)`. So `the deprecation this ratchet mirrors is still declared` asks
* the registry instead of grepping a `.tsx` for a console string, and
* `no LOADED registration declares a deprecation this list omits` is the new
* arm that direction makes possible at all.
*
* ⚠️ `DEPRECATED_TYPES` stays HAND-KEPT on purpose, and deriving it wholesale
* from the registry would be a regression rather than the obvious next step.
* This file loads `@object-ui/components` and nothing else; a type declared
* deprecated by a plugin package it does not import would silently drop out of
* a derived list, and the census would shrink to green. The list is the
* ratchet's authority precisely because it is complete by construction. What
* the declaration buys is that the list can now be CHECKED — in both directions
* — against something a machine can read.
*
* ## Why this ratchet freezes the stock instead of demanding zero
*
Expand DownExpand Up@@ -114,17 +133,30 @@
import { describe, it, expect } from 'vitest';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
// Registers `div` / `span` (and the rest of the basic set) at module scope, so
// the two arms below can ASK the registry what is deprecated instead of reading
// a renderer's source. Module scope, not a hook — objectui#3010/#3021.
import '@object-ui/components';
import { ComponentRegistry } from '@object-ui/core';

/** Resolved off this module, so the gate does not depend on the process cwd. */
const SCHEMAS_ROOT = fileURLToPath(new URL('../src/schemas', import.meta.url));
const COMPONENTS_SRC = fileURLToPath(
new URL('../../../packages/components/src/renderers/basic', import.meta.url),
);

/**
* The deprecated JSON-authored component types, mirrored by hand from the
* renderers' notices because the registry carries no `deprecated` flag. Kept
* honest by `the deprecation this ratchet mirrors is still declared`.
* The surface this corpus is authored on. Every fixture under `SCHEMAS_ROOT` is
* JSON metadata, so the question this ratchet asks the registry is scoped to
* it: `div` and `span` are ALSO permanent vocabulary of the `kind:'html'` tier
* (objectui#4000), where the parser compiles the plain tag straight through and
* no other spelling exists to migrate to. A gate that dropped the scope would
* be refusing a spelling that is correct on the other surface.
*/
const CORPUS_SURFACE = 'json' as const;

/**
* The deprecated JSON-authored component types this ratchet refuses. Hand-kept
* — see the header for why deriving it from the registry would shrink the
* census silently — and now checked in BOTH directions against the
* machine-readable declaration the registrations carry (objectui#6674).
*/
const DEPRECATED_TYPES = ['div', 'span'] as const;

Expand DownExpand Up@@ -293,19 +325,60 @@ describe('deprecated component types in the catalog are ratcheted (#3965)', () =

it('the deprecation this ratchet mirrors is still declared', () => {
// The mirror's premise. If a type is UN-deprecated, this file must die
// loudly rather than keep refusing a spelling that became legal again;
// `DEPRECATED_TYPES` is hand-kept precisely because the registry carries no
// machine-readable flag to derive it from.
for (const type of DEPRECATED_TYPES) {
const source = readFileSync(`${COMPONENTS_SRC}/${type}.tsx`, 'utf8');
expect(
source,
`renderers/basic/${type}.tsx no longer declares its deprecation notice. ` +
`Either the type was un-deprecated — in which case drop it from ` +
`DEPRECATED_TYPES and retire the matching baseline — or the notice ` +
`moved and this mirror needs re-pointing.`,
).toContain(`The "${type}" component is deprecated`);
}
// loudly rather than keep refusing a spelling that became legal again.
//
// This arm used to `readFileSync` the renderer and regex-match its
// `console.warn` literal, because that string was one of only two places a
// deprecation was stated and the only one a test could reach. It now asks
// the registry, which is objectui#6674's whole delivery: the question "is
// this type deprecated?" has an asker.
const undeclared = DEPRECATED_TYPES.filter(
(type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE) === undefined,
);

expect(
undeclared,
'A type this ratchet refuses no longer DECLARES a deprecation for the ' +
'json authoring surface. Either it was un-deprecated — in which case ' +
'drop it from DEPRECATED_TYPES and retire the matching baseline — or ' +
'the declaration moved and this mirror needs re-pointing. (A type ' +
'whose `surfaces` no longer lists `json` reads as un-deprecated HERE ' +
'and is still deprecated elsewhere; that is the objectui#4000 scope ' +
'working, not a bug in this arm.)',
).toEqual([]);
});

it('no LOADED registration declares a deprecation this list omits', () => {
// The direction the hand-kept mirror could never check. Before the
// declaration existed there was nothing to enumerate: a third deprecated
// type could have been added to `@object-ui/components` with a console
// string and a label, and this file would have gone on refusing exactly two.
//
// Scoped honestly to what this file LOADS — `@object-ui/components`. A
// plugin package's declaration is out of range here, which is the reason
// DEPRECATED_TYPES stays the authority rather than being derived.
//
// Non-vacuity is the arm ABOVE: an empty result here would also be produced
// by a registry that answered `undefined` for everything, and that state
// turns the premise arm red first. The two hold each other up.
const listed = new Set<string>(DEPRECATED_TYPES);
// A namespaced registration answers under BOTH spellings (`ui:div` and
// `div`); the corpus authors the bare one and the baseline is keyed on it.
// Either spelling being listed counts, and the raw key is what gets
// reported so the message names something that exists in the registry.
const bare = (key: string) => (key.includes(':') ? key.slice(key.indexOf(':') + 1) : key);
const missing = ComponentRegistry.getKnownTypes()
.filter((type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE))
.filter((type) => !listed.has(type) && !listed.has(bare(type)))
.sort();

expect(
missing,
'A loaded registration declares a json-surface deprecation that this ' +
'ratchet does not refuse. Add it to DEPRECATED_TYPES — and if the ' +
'corpus already authors it, baseline the existing stock in the same PR ' +
'rather than leaving the type unguarded.',
).toEqual([]);
});

it('the stock is exactly what this card measured, and the exemption is not a hole', () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -131,4 +132,38 @@ describe('div deprecation notice — scoped by provenance (#4000)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED, so a gate can read what the four
* cases above can only demonstrate by rendering.
*
* Before the registration carried `deprecated`, the only statements that this
* type is deprecated were the notice string literal and the word inside
* `label`; no gate, test or type could consult either, so both gates that
* touch component types ask whether the type RESOLVES instead — and it does.
*
* This case is the join. Above, the renderer EXEMPTS html-tier nodes at
* runtime; here, the registration DECLARES the identical scope. Asserting
* them in one file is what stops them drifting: widening `surfaces` to
* `['json', 'html']` without touching the exemption, or dropping the
* exemption without narrowing `surfaces`, turns this red — which no
* assertion about either one alone can do.
*/
it('DECLARES the scope those four cases demonstrate — deprecated on json, not on html', () => {
// Deprecated where the notice is fired…
expect(ComponentRegistry.deprecationFor('div', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
});

// …and NOT where the first case above proves the renderer stays silent. A
// declaration that said "deprecated" full stop would be false for the html
// tier, which is the objectui#4000 ruling this pair encodes.
expect(ComponentRegistry.deprecationFor('div', 'html')).toBeUndefined();

// The bare and namespaced spellings answer alike, because a corpus authors
// whichever it likes and the gate must not have to know which.
expect(ComponentRegistry.deprecationFor('ui:div', 'json')).toBeDefined();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -158,4 +159,25 @@ describe('span deprecation notice — scoped by provenance (#4917)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED. Level with the sibling case in
* `div-deprecation-provenance.test.tsx`, for the reason objectui#4917 gave
* for bringing this renderer level in the first place: the two carry the same
* ruling, and a fact stated for one of them and not the other is how they
* diverge.
*
* The runtime exemption above and the declaration below are the same fact.
* Moving either alone turns this red.
*/
it('DECLARES the scope those cases demonstrate — deprecated on json, not on html', () => {
expect(ComponentRegistry.deprecationFor('span', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
});

expect(ComponentRegistry.deprecationFor('span', 'html')).toBeUndefined();
expect(ComponentRegistry.deprecationFor('ui:span', 'json')).toBeDefined();
});
});
28 changes: 27 additions & 1 deletion packages/components/src/renderers/basic/div.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,11 +103,37 @@ const DivRenderer = forwardRef<HTMLDivElement, { schema: DivSchema; className?:
}
);

ComponentRegistry.register('div',
ComponentRegistry.register('div',
DivRenderer,
{
namespace: 'ui',
label: 'Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674).
*
* Until this key existed, the only two statements that this type is
* deprecated were `DIV_DEPRECATION_NOTICE` — a string literal inside a
* renderer — and the word inside `label`. Neither can be consulted by a
* gate, a test or a type, which is why a deprecated type could be authored
* 85 times across 27 shipped exemplars with every check in the repository
* green: both gates that touch component types ask whether the type
* RESOLVES, and this one resolves.
*
* `surfaces` carries the objectui#4000 ruling rather than restating it in a
* second place: the `isHtmlTierNode` exemption ABOVE and this list are the
* same fact, and `__tests__/div-deprecation-provenance.test.tsx` pins them
* to each other so neither can move alone.
*
* ⛔ Declaring this deprecates NOTHING NEW and fails NO build. The catalog
* ratchet (`examples/schema-catalog/test/deprecated-component-types.test.ts`,
* objectui#6732) freezes the existing stock and refuses growth; draining it
* is objectui#3965's worklist.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
Expand Down
18 changes: 17 additions & 1 deletion packages/components/src/renderers/basic/span.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -146,11 +146,27 @@ const SpanRenderer = forwardRef<HTMLSpanElement, { schema: TextSpanSchema; class
}
);

ComponentRegistry.register('span',
ComponentRegistry.register('span',
SpanRenderer,
{
namespace: 'ui',
label: 'Inline Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674),
* level with the sibling `div` registration for the same reason
* objectui#4917 brought this renderer level with it: `SPAN_DEPRECATION_NOTICE`
* is a string literal and `label` is prose, and no gate can read either.
*
* `surfaces` carries the objectui#4000 ruling — deprecated on the JSON
* authoring surface, permanent vocabulary of the `kind:'html'` tier — as the
* same fact the `isHtmlTierNode` exemption above applies at runtime.
* `__tests__/span-deprecation-provenance.test.tsx` pins the two together.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
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('^' + ".*" + ' feat(core): declare component deprecation in registry metadata, readable by a gate by claude[bot] · Pull Request #6822 · 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
25 changes: 25 additions & 0 deletions .changeset/6674-registry-deprecation-declaration.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
---
'@object-ui/components': minor
'@object-ui/core': minor
---

Component deprecation is now DECLARED, not just warned about (objectui#6674).

A deprecated component type used to be stated in exactly two places, neither of
which a gate, a test or a type can consult: a `console.warn` string literal
inside the renderer, and the word "(Deprecated)" inside a human-readable
`label`. Both gates that touch component types ask a different question —
whether the type RESOLVES — and a deprecated type resolves, which is how one
could be authored 85 times across 27 shipped exemplars with every check green.

- `@object-ui/core` gains `ComponentDeprecation` / `AuthoringSurface` and the
`deprecated` key on the registration metadata, plus
`ComponentRegistry.deprecationFor(type, surface)` to read it back. The
declaration carries the SURFACES it applies to rather than being a boolean:
`div` and `span` are deprecated on the JSON authoring surface and are at the
same time permanent vocabulary of the `kind:'html'` tier, so a bare flag would
be false for one of its two readers.
- `@object-ui/components` marks `div` and `span` with the declaration their
console notices already state. Nothing new is deprecated and no build starts
failing: the catalog ratchet keeps the existing stock frozen, and draining it
stays objectui#3965's worklist.
127 changes: 100 additions & 27 deletions examples/schema-catalog/test/deprecated-component-types.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,14 +28,33 @@
* and walks `content/docs` and nothing else (`DOCS_ROOT = 'content/docs'`),
* so `examples/**` is outside its scan surface entirely.
*
* It is worse than the usual declared-but-unenforced shape: the deprecation is
* not declared anywhere MACHINE-READABLE. `RegistryComponentMetaExtras` carries
* `tier` / `namespace` / `skipFallback` / `labelAssociation` and has no
* `deprecated` field, so the only statements of it are a `console.warn` string
* literal in `div.tsx` / `span.tsx` and the human-readable label
* `'Container (Deprecated)'`. `DEPRECATED_TYPES` below is therefore a hand-kept
* mirror, and `the deprecation this ratchet mirrors is still declared` is the
* arm that stops the mirror from outliving the thing it mirrors.
* It was worse than the usual declared-but-unenforced shape: when this file
* landed, the deprecation was not declared anywhere MACHINE-READABLE.
* `RegistryComponentMetaExtras` carried `tier` / `namespace` / `skipFallback` /
* `labelling` and no `deprecated` field, so the only statements of it were a
* `console.warn` string literal in `div.tsx` / `span.tsx` and the
* human-readable label `'Container (Deprecated)'`. This file was therefore
* built on a hand-kept mirror whose premise arm READ THE RENDERER'S SOURCE and
* regex-matched that console literal — the closest thing to asking "is this
* type deprecated?" that existed.
*
* ## What objectui#6674 changed, and what it did not
*
* The registration now DECLARES it: `deprecated: { surfaces: ['json'],
* replacement: … }`, read back through `ComponentRegistry.deprecationFor(type,
* surface)`. So `the deprecation this ratchet mirrors is still declared` asks
* the registry instead of grepping a `.tsx` for a console string, and
* `no LOADED registration declares a deprecation this list omits` is the new
* arm that direction makes possible at all.
*
* ⚠️ `DEPRECATED_TYPES` stays HAND-KEPT on purpose, and deriving it wholesale
* from the registry would be a regression rather than the obvious next step.
* This file loads `@object-ui/components` and nothing else; a type declared
* deprecated by a plugin package it does not import would silently drop out of
* a derived list, and the census would shrink to green. The list is the
* ratchet's authority precisely because it is complete by construction. What
* the declaration buys is that the list can now be CHECKED — in both directions
* — against something a machine can read.
*
* ## Why this ratchet freezes the stock instead of demanding zero
*
Expand DownExpand Up@@ -114,17 +133,30 @@
import { describe, it, expect } from 'vitest';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
// Registers `div` / `span` (and the rest of the basic set) at module scope, so
// the two arms below can ASK the registry what is deprecated instead of reading
// a renderer's source. Module scope, not a hook — objectui#3010/#3021.
import '@object-ui/components';
import { ComponentRegistry } from '@object-ui/core';

/** Resolved off this module, so the gate does not depend on the process cwd. */
const SCHEMAS_ROOT = fileURLToPath(new URL('../src/schemas', import.meta.url));
const COMPONENTS_SRC = fileURLToPath(
new URL('../../../packages/components/src/renderers/basic', import.meta.url),
);

/**
* The deprecated JSON-authored component types, mirrored by hand from the
* renderers' notices because the registry carries no `deprecated` flag. Kept
* honest by `the deprecation this ratchet mirrors is still declared`.
* The surface this corpus is authored on. Every fixture under `SCHEMAS_ROOT` is
* JSON metadata, so the question this ratchet asks the registry is scoped to
* it: `div` and `span` are ALSO permanent vocabulary of the `kind:'html'` tier
* (objectui#4000), where the parser compiles the plain tag straight through and
* no other spelling exists to migrate to. A gate that dropped the scope would
* be refusing a spelling that is correct on the other surface.
*/
const CORPUS_SURFACE = 'json' as const;

/**
* The deprecated JSON-authored component types this ratchet refuses. Hand-kept
* — see the header for why deriving it from the registry would shrink the
* census silently — and now checked in BOTH directions against the
* machine-readable declaration the registrations carry (objectui#6674).
*/
const DEPRECATED_TYPES = ['div', 'span'] as const;

Expand DownExpand Up@@ -293,19 +325,60 @@ describe('deprecated component types in the catalog are ratcheted (#3965)', () =

it('the deprecation this ratchet mirrors is still declared', () => {
// The mirror's premise. If a type is UN-deprecated, this file must die
// loudly rather than keep refusing a spelling that became legal again;
// `DEPRECATED_TYPES` is hand-kept precisely because the registry carries no
// machine-readable flag to derive it from.
for (const type of DEPRECATED_TYPES) {
const source = readFileSync(`${COMPONENTS_SRC}/${type}.tsx`, 'utf8');
expect(
source,
`renderers/basic/${type}.tsx no longer declares its deprecation notice. ` +
`Either the type was un-deprecated — in which case drop it from ` +
`DEPRECATED_TYPES and retire the matching baseline — or the notice ` +
`moved and this mirror needs re-pointing.`,
).toContain(`The "${type}" component is deprecated`);
}
// loudly rather than keep refusing a spelling that became legal again.
//
// This arm used to `readFileSync` the renderer and regex-match its
// `console.warn` literal, because that string was one of only two places a
// deprecation was stated and the only one a test could reach. It now asks
// the registry, which is objectui#6674's whole delivery: the question "is
// this type deprecated?" has an asker.
const undeclared = DEPRECATED_TYPES.filter(
(type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE) === undefined,
);

expect(
undeclared,
'A type this ratchet refuses no longer DECLARES a deprecation for the ' +
'json authoring surface. Either it was un-deprecated — in which case ' +
'drop it from DEPRECATED_TYPES and retire the matching baseline — or ' +
'the declaration moved and this mirror needs re-pointing. (A type ' +
'whose `surfaces` no longer lists `json` reads as un-deprecated HERE ' +
'and is still deprecated elsewhere; that is the objectui#4000 scope ' +
'working, not a bug in this arm.)',
).toEqual([]);
});

it('no LOADED registration declares a deprecation this list omits', () => {
// The direction the hand-kept mirror could never check. Before the
// declaration existed there was nothing to enumerate: a third deprecated
// type could have been added to `@object-ui/components` with a console
// string and a label, and this file would have gone on refusing exactly two.
//
// Scoped honestly to what this file LOADS — `@object-ui/components`. A
// plugin package's declaration is out of range here, which is the reason
// DEPRECATED_TYPES stays the authority rather than being derived.
//
// Non-vacuity is the arm ABOVE: an empty result here would also be produced
// by a registry that answered `undefined` for everything, and that state
// turns the premise arm red first. The two hold each other up.
const listed = new Set<string>(DEPRECATED_TYPES);
// A namespaced registration answers under BOTH spellings (`ui:div` and
// `div`); the corpus authors the bare one and the baseline is keyed on it.
// Either spelling being listed counts, and the raw key is what gets
// reported so the message names something that exists in the registry.
const bare = (key: string) => (key.includes(':') ? key.slice(key.indexOf(':') + 1) : key);
const missing = ComponentRegistry.getKnownTypes()
.filter((type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE))
.filter((type) => !listed.has(type) && !listed.has(bare(type)))
.sort();

expect(
missing,
'A loaded registration declares a json-surface deprecation that this ' +
'ratchet does not refuse. Add it to DEPRECATED_TYPES — and if the ' +
'corpus already authors it, baseline the existing stock in the same PR ' +
'rather than leaving the type unguarded.',
).toEqual([]);
});

it('the stock is exactly what this card measured, and the exemption is not a hole', () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -131,4 +132,38 @@ describe('div deprecation notice — scoped by provenance (#4000)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED, so a gate can read what the four
* cases above can only demonstrate by rendering.
*
* Before the registration carried `deprecated`, the only statements that this
* type is deprecated were the notice string literal and the word inside
* `label`; no gate, test or type could consult either, so both gates that
* touch component types ask whether the type RESOLVES instead — and it does.
*
* This case is the join. Above, the renderer EXEMPTS html-tier nodes at
* runtime; here, the registration DECLARES the identical scope. Asserting
* them in one file is what stops them drifting: widening `surfaces` to
* `['json', 'html']` without touching the exemption, or dropping the
* exemption without narrowing `surfaces`, turns this red — which no
* assertion about either one alone can do.
*/
it('DECLARES the scope those four cases demonstrate — deprecated on json, not on html', () => {
// Deprecated where the notice is fired…
expect(ComponentRegistry.deprecationFor('div', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
});

// …and NOT where the first case above proves the renderer stays silent. A
// declaration that said "deprecated" full stop would be false for the html
// tier, which is the objectui#4000 ruling this pair encodes.
expect(ComponentRegistry.deprecationFor('div', 'html')).toBeUndefined();

// The bare and namespaced spellings answer alike, because a corpus authors
// whichever it likes and the gate must not have to know which.
expect(ComponentRegistry.deprecationFor('ui:div', 'json')).toBeDefined();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -158,4 +159,25 @@ describe('span deprecation notice — scoped by provenance (#4917)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED. Level with the sibling case in
* `div-deprecation-provenance.test.tsx`, for the reason objectui#4917 gave
* for bringing this renderer level in the first place: the two carry the same
* ruling, and a fact stated for one of them and not the other is how they
* diverge.
*
* The runtime exemption above and the declaration below are the same fact.
* Moving either alone turns this red.
*/
it('DECLARES the scope those cases demonstrate — deprecated on json, not on html', () => {
expect(ComponentRegistry.deprecationFor('span', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
});

expect(ComponentRegistry.deprecationFor('span', 'html')).toBeUndefined();
expect(ComponentRegistry.deprecationFor('ui:span', 'json')).toBeDefined();
});
});
28 changes: 27 additions & 1 deletion packages/components/src/renderers/basic/div.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,11 +103,37 @@ const DivRenderer = forwardRef<HTMLDivElement, { schema: DivSchema; className?:
}
);

ComponentRegistry.register('div',
ComponentRegistry.register('div',
DivRenderer,
{
namespace: 'ui',
label: 'Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674).
*
* Until this key existed, the only two statements that this type is
* deprecated were `DIV_DEPRECATION_NOTICE` — a string literal inside a
* renderer — and the word inside `label`. Neither can be consulted by a
* gate, a test or a type, which is why a deprecated type could be authored
* 85 times across 27 shipped exemplars with every check in the repository
* green: both gates that touch component types ask whether the type
* RESOLVES, and this one resolves.
*
* `surfaces` carries the objectui#4000 ruling rather than restating it in a
* second place: the `isHtmlTierNode` exemption ABOVE and this list are the
* same fact, and `__tests__/div-deprecation-provenance.test.tsx` pins them
* to each other so neither can move alone.
*
* ⛔ Declaring this deprecates NOTHING NEW and fails NO build. The catalog
* ratchet (`examples/schema-catalog/test/deprecated-component-types.test.ts`,
* objectui#6732) freezes the existing stock and refuses growth; draining it
* is objectui#3965's worklist.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
Expand Down
18 changes: 17 additions & 1 deletion packages/components/src/renderers/basic/span.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -146,11 +146,27 @@ const SpanRenderer = forwardRef<HTMLSpanElement, { schema: TextSpanSchema; class
}
);

ComponentRegistry.register('span',
ComponentRegistry.register('span',
SpanRenderer,
{
namespace: 'ui',
label: 'Inline Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674),
* level with the sibling `div` registration for the same reason
* objectui#4917 brought this renderer level with it: `SPAN_DEPRECATION_NOTICE`
* is a string literal and `label` is prose, and no gate can read either.
*
* `surfaces` carries the objectui#4000 ruling — deprecated on the JSON
* authoring surface, permanent vocabulary of the `kind:'html'` tier — as the
* same fact the `isHtmlTierNode` exemption above applies at runtime.
* `__tests__/span-deprecation-provenance.test.tsx` pins the two together.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
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); } })(); })(); feat(core): declare component deprecation in registry metadata, readable by a gate by claude[bot] · Pull Request #6822 · 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
25 changes: 25 additions & 0 deletions .changeset/6674-registry-deprecation-declaration.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
---
'@object-ui/components': minor
'@object-ui/core': minor
---

Component deprecation is now DECLARED, not just warned about (objectui#6674).

A deprecated component type used to be stated in exactly two places, neither of
which a gate, a test or a type can consult: a `console.warn` string literal
inside the renderer, and the word "(Deprecated)" inside a human-readable
`label`. Both gates that touch component types ask a different question —
whether the type RESOLVES — and a deprecated type resolves, which is how one
could be authored 85 times across 27 shipped exemplars with every check green.

- `@object-ui/core` gains `ComponentDeprecation` / `AuthoringSurface` and the
`deprecated` key on the registration metadata, plus
`ComponentRegistry.deprecationFor(type, surface)` to read it back. The
declaration carries the SURFACES it applies to rather than being a boolean:
`div` and `span` are deprecated on the JSON authoring surface and are at the
same time permanent vocabulary of the `kind:'html'` tier, so a bare flag would
be false for one of its two readers.
- `@object-ui/components` marks `div` and `span` with the declaration their
console notices already state. Nothing new is deprecated and no build starts
failing: the catalog ratchet keeps the existing stock frozen, and draining it
stays objectui#3965's worklist.
127 changes: 100 additions & 27 deletions examples/schema-catalog/test/deprecated-component-types.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,14 +28,33 @@
* and walks `content/docs` and nothing else (`DOCS_ROOT = 'content/docs'`),
* so `examples/**` is outside its scan surface entirely.
*
* It is worse than the usual declared-but-unenforced shape: the deprecation is
* not declared anywhere MACHINE-READABLE. `RegistryComponentMetaExtras` carries
* `tier` / `namespace` / `skipFallback` / `labelAssociation` and has no
* `deprecated` field, so the only statements of it are a `console.warn` string
* literal in `div.tsx` / `span.tsx` and the human-readable label
* `'Container (Deprecated)'`. `DEPRECATED_TYPES` below is therefore a hand-kept
* mirror, and `the deprecation this ratchet mirrors is still declared` is the
* arm that stops the mirror from outliving the thing it mirrors.
* It was worse than the usual declared-but-unenforced shape: when this file
* landed, the deprecation was not declared anywhere MACHINE-READABLE.
* `RegistryComponentMetaExtras` carried `tier` / `namespace` / `skipFallback` /
* `labelling` and no `deprecated` field, so the only statements of it were a
* `console.warn` string literal in `div.tsx` / `span.tsx` and the
* human-readable label `'Container (Deprecated)'`. This file was therefore
* built on a hand-kept mirror whose premise arm READ THE RENDERER'S SOURCE and
* regex-matched that console literal — the closest thing to asking "is this
* type deprecated?" that existed.
*
* ## What objectui#6674 changed, and what it did not
*
* The registration now DECLARES it: `deprecated: { surfaces: ['json'],
* replacement: … }`, read back through `ComponentRegistry.deprecationFor(type,
* surface)`. So `the deprecation this ratchet mirrors is still declared` asks
* the registry instead of grepping a `.tsx` for a console string, and
* `no LOADED registration declares a deprecation this list omits` is the new
* arm that direction makes possible at all.
*
* ⚠️ `DEPRECATED_TYPES` stays HAND-KEPT on purpose, and deriving it wholesale
* from the registry would be a regression rather than the obvious next step.
* This file loads `@object-ui/components` and nothing else; a type declared
* deprecated by a plugin package it does not import would silently drop out of
* a derived list, and the census would shrink to green. The list is the
* ratchet's authority precisely because it is complete by construction. What
* the declaration buys is that the list can now be CHECKED — in both directions
* — against something a machine can read.
*
* ## Why this ratchet freezes the stock instead of demanding zero
*
Expand DownExpand Up@@ -114,17 +133,30 @@
import { describe, it, expect } from 'vitest';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
// Registers `div` / `span` (and the rest of the basic set) at module scope, so
// the two arms below can ASK the registry what is deprecated instead of reading
// a renderer's source. Module scope, not a hook — objectui#3010/#3021.
import '@object-ui/components';
import { ComponentRegistry } from '@object-ui/core';

/** Resolved off this module, so the gate does not depend on the process cwd. */
const SCHEMAS_ROOT = fileURLToPath(new URL('../src/schemas', import.meta.url));
const COMPONENTS_SRC = fileURLToPath(
new URL('../../../packages/components/src/renderers/basic', import.meta.url),
);

/**
* The deprecated JSON-authored component types, mirrored by hand from the
* renderers' notices because the registry carries no `deprecated` flag. Kept
* honest by `the deprecation this ratchet mirrors is still declared`.
* The surface this corpus is authored on. Every fixture under `SCHEMAS_ROOT` is
* JSON metadata, so the question this ratchet asks the registry is scoped to
* it: `div` and `span` are ALSO permanent vocabulary of the `kind:'html'` tier
* (objectui#4000), where the parser compiles the plain tag straight through and
* no other spelling exists to migrate to. A gate that dropped the scope would
* be refusing a spelling that is correct on the other surface.
*/
const CORPUS_SURFACE = 'json' as const;

/**
* The deprecated JSON-authored component types this ratchet refuses. Hand-kept
* — see the header for why deriving it from the registry would shrink the
* census silently — and now checked in BOTH directions against the
* machine-readable declaration the registrations carry (objectui#6674).
*/
const DEPRECATED_TYPES = ['div', 'span'] as const;

Expand DownExpand Up@@ -293,19 +325,60 @@ describe('deprecated component types in the catalog are ratcheted (#3965)', () =

it('the deprecation this ratchet mirrors is still declared', () => {
// The mirror's premise. If a type is UN-deprecated, this file must die
// loudly rather than keep refusing a spelling that became legal again;
// `DEPRECATED_TYPES` is hand-kept precisely because the registry carries no
// machine-readable flag to derive it from.
for (const type of DEPRECATED_TYPES) {
const source = readFileSync(`${COMPONENTS_SRC}/${type}.tsx`, 'utf8');
expect(
source,
`renderers/basic/${type}.tsx no longer declares its deprecation notice. ` +
`Either the type was un-deprecated — in which case drop it from ` +
`DEPRECATED_TYPES and retire the matching baseline — or the notice ` +
`moved and this mirror needs re-pointing.`,
).toContain(`The "${type}" component is deprecated`);
}
// loudly rather than keep refusing a spelling that became legal again.
//
// This arm used to `readFileSync` the renderer and regex-match its
// `console.warn` literal, because that string was one of only two places a
// deprecation was stated and the only one a test could reach. It now asks
// the registry, which is objectui#6674's whole delivery: the question "is
// this type deprecated?" has an asker.
const undeclared = DEPRECATED_TYPES.filter(
(type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE) === undefined,
);

expect(
undeclared,
'A type this ratchet refuses no longer DECLARES a deprecation for the ' +
'json authoring surface. Either it was un-deprecated — in which case ' +
'drop it from DEPRECATED_TYPES and retire the matching baseline — or ' +
'the declaration moved and this mirror needs re-pointing. (A type ' +
'whose `surfaces` no longer lists `json` reads as un-deprecated HERE ' +
'and is still deprecated elsewhere; that is the objectui#4000 scope ' +
'working, not a bug in this arm.)',
).toEqual([]);
});

it('no LOADED registration declares a deprecation this list omits', () => {
// The direction the hand-kept mirror could never check. Before the
// declaration existed there was nothing to enumerate: a third deprecated
// type could have been added to `@object-ui/components` with a console
// string and a label, and this file would have gone on refusing exactly two.
//
// Scoped honestly to what this file LOADS — `@object-ui/components`. A
// plugin package's declaration is out of range here, which is the reason
// DEPRECATED_TYPES stays the authority rather than being derived.
//
// Non-vacuity is the arm ABOVE: an empty result here would also be produced
// by a registry that answered `undefined` for everything, and that state
// turns the premise arm red first. The two hold each other up.
const listed = new Set<string>(DEPRECATED_TYPES);
// A namespaced registration answers under BOTH spellings (`ui:div` and
// `div`); the corpus authors the bare one and the baseline is keyed on it.
// Either spelling being listed counts, and the raw key is what gets
// reported so the message names something that exists in the registry.
const bare = (key: string) => (key.includes(':') ? key.slice(key.indexOf(':') + 1) : key);
const missing = ComponentRegistry.getKnownTypes()
.filter((type) => ComponentRegistry.deprecationFor(type, CORPUS_SURFACE))
.filter((type) => !listed.has(type) && !listed.has(bare(type)))
.sort();

expect(
missing,
'A loaded registration declares a json-surface deprecation that this ' +
'ratchet does not refuse. Add it to DEPRECATED_TYPES — and if the ' +
'corpus already authors it, baseline the existing stock in the same PR ' +
'rather than leaving the type unguarded.',
).toEqual([]);
});

it('the stock is exactly what this card measured, and the exemption is not a hole', () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -131,4 +132,38 @@ describe('div deprecation notice — scoped by provenance (#4000)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED, so a gate can read what the four
* cases above can only demonstrate by rendering.
*
* Before the registration carried `deprecated`, the only statements that this
* type is deprecated were the notice string literal and the word inside
* `label`; no gate, test or type could consult either, so both gates that
* touch component types ask whether the type RESOLVES instead — and it does.
*
* This case is the join. Above, the renderer EXEMPTS html-tier nodes at
* runtime; here, the registration DECLARES the identical scope. Asserting
* them in one file is what stops them drifting: widening `surfaces` to
* `['json', 'html']` without touching the exemption, or dropping the
* exemption without narrowing `surfaces`, turns this red — which no
* assertion about either one alone can do.
*/
it('DECLARES the scope those four cases demonstrate — deprecated on json, not on html', () => {
// Deprecated where the notice is fired…
expect(ComponentRegistry.deprecationFor('div', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
});

// …and NOT where the first case above proves the renderer stays silent. A
// declaration that said "deprecated" full stop would be false for the html
// tier, which is the objectui#4000 ruling this pair encodes.
expect(ComponentRegistry.deprecationFor('div', 'html')).toBeUndefined();

// The bare and namespaced spellings answer alike, because a corpus authors
// whichever it likes and the gate must not have to know which.
expect(ComponentRegistry.deprecationFor('ui:div', 'json')).toBeDefined();
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { SchemaRenderer } from '@object-ui/react';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
Expand DownExpand Up@@ -158,4 +159,25 @@ describe('span deprecation notice — scoped by provenance (#4917)', () => {
const attrs = Array.from(el!.attributes).map((a) => a.name);
expect(attrs.filter((n) => n.includes('provenance') || n.includes('tier'))).toHaveLength(0);
});

/**
* objectui#6674 — the same scope, DECLARED. Level with the sibling case in
* `div-deprecation-provenance.test.tsx`, for the reason objectui#4917 gave
* for bringing this renderer level in the first place: the two carry the same
* ruling, and a fact stated for one of them and not the other is how they
* diverge.
*
* The runtime exemption above and the declaration below are the same fact.
* Moving either alone turns this red.
*/
it('DECLARES the scope those cases demonstrate — deprecated on json, not on html', () => {
expect(ComponentRegistry.deprecationFor('span', 'json')).toEqual({
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
});

expect(ComponentRegistry.deprecationFor('span', 'html')).toBeUndefined();
expect(ComponentRegistry.deprecationFor('ui:span', 'json')).toBeDefined();
});
});
28 changes: 27 additions & 1 deletion packages/components/src/renderers/basic/div.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,11 +103,37 @@ const DivRenderer = forwardRef<HTMLDivElement, { schema: DivSchema; className?:
}
);

ComponentRegistry.register('div',
ComponentRegistry.register('div',
DivRenderer,
{
namespace: 'ui',
label: 'Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674).
*
* Until this key existed, the only two statements that this type is
* deprecated were `DIV_DEPRECATION_NOTICE` — a string literal inside a
* renderer — and the word inside `label`. Neither can be consulted by a
* gate, a test or a type, which is why a deprecated type could be authored
* 85 times across 27 shipped exemplars with every check in the repository
* green: both gates that touch component types ask whether the type
* RESOLVES, and this one resolves.
*
* `surfaces` carries the objectui#4000 ruling rather than restating it in a
* second place: the `isHtmlTierNode` exemption ABOVE and this list are the
* same fact, and `__tests__/div-deprecation-provenance.test.tsx` pins them
* to each other so neither can move alone.
*
* ⛔ Declaring this deprecates NOTHING NEW and fails NO build. The catalog
* ratchet (`examples/schema-catalog/test/deprecated-component-types.test.ts`,
* objectui#6732) freezes the existing stock and refuses growth; draining it
* is objectui#3965's worklist.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "card", "flex", or layout components like "container", "stack", or "grid"',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
Expand Down
18 changes: 17 additions & 1 deletion packages/components/src/renderers/basic/span.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -146,11 +146,27 @@ const SpanRenderer = forwardRef<HTMLSpanElement, { schema: TextSpanSchema; class
}
);

ComponentRegistry.register('span',
ComponentRegistry.register('span',
SpanRenderer,
{
namespace: 'ui',
label: 'Inline Container (Deprecated)',
/**
* The MACHINE-READABLE statement of the deprecation above (objectui#6674),
* level with the sibling `div` registration for the same reason
* objectui#4917 brought this renderer level with it: `SPAN_DEPRECATION_NOTICE`
* is a string literal and `label` is prose, and no gate can read either.
*
* `surfaces` carries the objectui#4000 ruling — deprecated on the JSON
* authoring surface, permanent vocabulary of the `kind:'html'` tier — as the
* same fact the `isHtmlTierNode` exemption above applies at runtime.
* `__tests__/span-deprecation-provenance.test.tsx` pins the two together.
*/
deprecated: {
surfaces: ['json'],
replacement:
'use "badge" for labels, or "text" with a className for inline emphasis',
},
inputs: [
{ name: 'className', type: 'string', label: 'CSS Class' }
],
Expand Down
Loading
Loading