Uh oh!
There was an error while loading. Please reload this page.
fix(types,core): one authority for ComponentConfig - #6937
Merged
Conversation
`@object-ui/types` and `@object-ui/core` each published a declaration of `ComponentConfig`. PR #6297 single-sourced the `ComponentMeta` half; what survived it was genericity and the `component` slot — types' was non-generic with `component: any`, core's was `<T = any>` with `component: ComponentRenderer<T>`. `@object-ui/types`' declaration gains that type parameter, defaulted, so every existing spelling keeps its meaning exactly. `@object-ui/core` re-exports it instead of declaring its own, and the registry-only keys move to a named extension, `RegistryComponentConfig`, which the registry getters return — type-identical to what they returned before. `ComponentRenderer<T>` is the identity alias, which is why `component: T` in `@object-ui/types` says what `component: ComponentRenderer<T>` said in core without any dependency edge from types to core (that would be a cycle). The identity is pinned so it cannot stop being true unnoticed. Removes the `ComponentConfig` row from `KNOWN_COLLISIONS` in `scripts/__tests__/one-authority-per-exported-name-6273.test.ts`, which fails in both directions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…naming the break Contract review, clause ②: `patch` mis-signals into the release notes. This PR removes five keys from a published type name (`ComponentConfig` as exported by `@object-ui/core`) and adds both a type parameter and a new exported name, so the bump is not a patch. AGENTS.md's versioning policy and `scripts/check-changeset-no-major.mjs`'s header both say objectui's own breaking changes ship as `minor` with the break spelled out in the body, because the whole publishable set is one changeset `fixed` group pinned to `@objectstack`'s major. Adds that sentence, naming the class plainly. Changeset file only; no code, test or baseline change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
This was referenced Aug 31, 2026
os-sam pushed a commit
that referenced
this pull request
Aug 31, 2026
…inor Two fixes to the review of #6318's PR. 1. objectui#6273 (the 2026-08-25 family ruling objectui#6172 / 甲-A1) went red: the two schemas this branch added to `@object-ui/types` collided by name with the pre-existing declarations in their owning plugins. The two shapes were compared STRUCTURALLY before choosing, not by reading: a tsc probe asserting mutual assignability in both directions, per pair, plus a per-member probe for `language` — the one member the two spelled differently (`'javascript' | … | string`, which TypeScript collapses to exactly `string`). Every relation held, and a deliberate counter-probe against an unrelated shape failed as required, so the green was a measurement rather than a vacuous assertion. Same heritage, same member set, same per-member types and optionality ⇒ these are re-points, not renames. Direction is forced: `@object-ui/types` is the lower layer and cannot import from a plugin without a cycle, so the plugins re-point at types'. `export type { X } from '@object-ui/types'` is a re-export, which the gate does not count as a second declaration. ⛔ `KNOWN_COLLISIONS` is untouched (shrink-only), and so is the gate's own test file — #6936 and #6937 both have unlanded edits to it. The published import paths are unchanged: `check:readme-exports` still resolves both README self-imports through the re-export (386 judged, 0 wrong-path, 0 fabricated), and `objectui check` reports a byte-identical file list, so the accept set of the validator did not move. 2. Regrade the changeset `patch` → `minor` for `@object-ui/types`, per contract review: the package's own precedent (the `visible` widen, and `ViewNavigationConfig`) grades a change of this size `minor` on the stated grounds that it is more than patch describes. Purely additive, so no migration note is owed beyond the regrade. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-sam
marked this pull request as ready for review
August 31, 2026 03:35
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#6298
The measurement the card turns on, made before the fix
Triage defaulted to direction (b)"provided
ComponentRendererhas a home@object-ui/typescan reach". That proviso is a measurement, so it was made first.M1 — where
ComponentRendererlives. Exactly ONE declaration in the repo:packages/core/src/registry/Registry.ts:16, an alias whose body is its own type parameter. Control probe for the search shape (a zero hit is a reading only after a known-present neighbour is probed): the same pattern run for names endingComponentConfigreturns four declarations, spanning both theexport typeand theexport interfacespelling.M2 — can
@object-ui/typesreach it? No.packages/types/package.jsondepends on@objectstack/specandzod, nothing else;packages/core/package.jsondepends on@object-ui/typesatworkspace:*. An edge in the other direction is a cycle, and AGENTS.md section 3 pins@object-ui/typesas the bottom layer — "Zero deps. No React."So the proviso fails on its literal reading.M3 — but the blocker the proviso guards is vacuous.
ComponentRendereris the identity alias — it resolves to its own type argument and contributes no type information. Socomponent: Tin@object-ui/typessays exactly whatcomponent: ComponentRendererofTsaid in core, with nothing to import. (b) is reachable with no new dependency edge.M4 — and (a) could not deliver the baseline deletion this card mandates. The gate's
authoritySitescountsexport type X =/export interface Xas an authority and deliberately does not countexport type { X } from .... That is not a prediction:ComponentMetawas converged the (a) way by PR #6297 and is still aKNOWN_COLLISIONSrow on this tree, one line below the row this PR removes. A derived declaration in core would have left the collision measured and the row owed.Selection: (b) — the only one of the two that satisfies the mandatory baseline deletion, and its stated proviso turns out not to bind.
What landed
packages/types/src/base.ts—ComponentConfiggains the type parameter, defaulted, and itscomponentslot becomes that parameter. BareComponentConfigis the parameter instantiated atany, so the slot is stillany: every existing spelling keeps its meaning exactly.packages/core/src/registry/Registry.ts— the local declaration becomesexport type { ComponentConfig } from '@object-ui/types';, the same disposition objectui#5671 gaveComponentInputa few lines above.M5 — the registry-only keys were rehomed, not dropped. A bare re-export would have taken
tier/namespace/skipFallback/labelling/deprecatedoff the registry ENTRY type, andRegistry.getNamespaceComponentsfilters onconfig.namespace. They now live on a named extension,RegistryComponentConfig— the shape PR #6297 gaveComponentMeta: one declaration for the shared members, a named extension for the rest. It is type-identical to theComponentConfigthis file used to declare, and it is whatgetConfig/getAllConfigs/getNamespaceComponentsreturn, so every read path is unchanged.ComponentRendereritself is unchanged, but is now documented as load-bearing and pinned.The published surface, enumerated before and after
Both declarations are published, so this moves a published surface. Read by an out-of-package consumer compiled against the emitted
.d.tsof both packages (not against source):@object-ui/types' declaration accepts a type argumentTS2315: Type 'ComponentConfig' is not genericnevernevernevercomponentslotanyanycomponentslotanyanytruetrueThree readings moved, and they are exactly the two deltas the card named: genericity, and the key-set difference collapsing.
The last row is the ghost, recorded deliberately: mutual assignability was
trueon the diverged pair —component: anyabsorbs everything and every other member is optional — so an assignability assertion could not have caught this and cannot catch its return. That is the same contrastcomponent-meta-derives-from-canonical.test.tsrecords for the sibling type, and it is why the new pin compares key sets and source identity instead, keeping the assignability pair only as a labelled control."No consumer changes" — proved, not asserted
packages/**,apps/**,examples/**ore2e/**importsComponentConfigfrom@object-ui/core; the only textual hits are prose inside the doc comment this PR adds. Control probe for the search shape, same module, same form:ComponentRegistry388 sites,Registry7,ComponentMeta6. The zero is a reading..d.ts: every registry read path (getConfig,getAllConfigs,getNamespaceComponents, each of the five registry keys, the canonical keys,type,component), assignment of a registry value to the name from both packages, and the bare-spelling object literal —tscexit 0. Negative control: reading a key that exists on neither declaration fails withTS2339 ... does not exist on type 'RegistryComponentConfig', so the probe is live rather than vacuous.@object-ui/sdui-parser,@object-ui/components,@object-ui/react,@object-ui/app-shell— all green.Ablation — direction predicted in writing before the run
Predicted: delete the
KNOWN_COLLISIONSrow without converging and the gate goes red in thefreshdirection (a NEW colliding name), not thestaleone.Observed: exactly that.
Mutation proved on disk before the run (anchored line count 1 to 0;
git hash-objectmoved off theHEADblob) and the restore proved both ways after it (hash back to theHEADblob, anchored count back to 1,git diff HEADempty), undertrap ... EXIT INT TERMwith absolute paths andgit checkout HEAD -- ABSOLUTE_PATH. The fact was mutated, never the assertion. The gate's own remedy text names the route this PR took: "export type { X } from ...is a re-export, not a second declaration, and this gate does not count it."Baseline
ComponentConfigdeleted fromKNOWN_COLLISIONS— only that row (43 rows to 42;ComponentMeta,ComboboxOptionand theActionSchema/BreadcrumbItem/BreadcrumbSchemarows owned by objectui#6349's parallel batch all verified intact). A comment records why it left and why theComponentMetarow on the very next line deliberately stays, following theFormFieldSpecprecedent in the same table. No file owned by that parallel batch is touched.Gates — exit codes captured before any pipe
@object-ui/typestype-check@object-ui/coretype-checkpackages/types/ packages/core/ scripts/node scripts/check-phantom-dependencies.mjspnpm exec eslint .(plain form)node scripts/check-changeset-presence.mjscheck:changeset-fixed/no-major/overwritecheck:control-bytescheck:doc-fences/check:doc-types/check:doc-snippetscheck:readme-exportscheck:self-import/check:spec-symbolstype-check, 4 registry consumersThe before/after vitest counts were taken by running the same scope on the merge-base in a throwaway comparison worktree, since the working tree cannot be in two states at once; that worktree was removed.
Declared narrowing: the vitest scope is
packages/types/,packages/core/andscripts/rather than the whole suite, andtype-checkwas run for the two edited packages plus four downstream consumers rather than the whole farm. CI runs both in full regardless. Everything in the table above was measured on6808939d9, the final commit.Type-check coverage was verified, not assumed:
tsc -p packages/core/tsconfig.test.json --listFilesconfirms the new pin file is in the program and that it readspackages/types/dist/base.d.ts— the emitted declaration, carrying the new type parameter — so the type-level assertions are enforced rather than erased.Docs and changeset
content/docs/guide/plugin-development.md— the return type in the "Querying Registered Components" example follows the getter. Changeset added (patchon both packages); the surface change is additive except for the five registry keys moving from the nameComponentConfigtoRegistryComponentConfig, which is what every getter returns.Generated by Claude Code