feat(a2ui): pluggable defineCatalog API + extractor bin shim fix#2560
Conversation
Introduce a runtime catalog API alongside the existing global
`componentRegistry`-based path. Composition is per-component so bundlers
tree-shake unused built-ins; the protocol name comes from each manifest
key (or `displayName ?? component.name` for bare components).
- `defineCatalog(inputs)` accepts bare components, `[component, manifest]`
tuples, or already-resolved entries.
- `mergeCatalogs(...)` last-write-wins for layered overrides.
- `serializeCatalog(...)` emits the agent-handshake JSON, including
per-component schemas when manifests are paired in.
- `resolveCatalog(...)` builds the name → component map the renderer uses.
Also harden the extractor's bin entry detection: `isEntryScript()` now
recognizes the published `bin/a2ui-catalog-extractor.{m,c,}js` shim so
running via the bin path works alongside direct ESM execution.
The new exports are additive — `export * from './all.js'` is preserved so
existing consumers of the global registry are unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (14)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds a typed catalog system and documentation for a2ui, re-exports its API, introduces compatibility barrels for React and store types used by catalog components, and refactors the CLI entry detection into a small isEntryScript() helper. ChangesCLI Entry Refactor
Catalog System (new API + docs)
Compatibility / Import Surface Updates
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/genui/a2ui/src/catalog/defineCatalog.ts (1)
72-112: ⚡ Quick winTighten runtime validation in
isTuple/resolveInput.Two small robustness gaps in the tuple path:
isTupleonly checksArray.isArray, so a malformed input like[Component]or[]is treated as a tuple and the destructure on line 97 yieldsmanifest === undefined. The next line then throws a genericTypeError: Cannot convert undefined or null to objectinstead of the friendly[a2ui] …error users get for the empty-manifest case.- The manifest is documented as a single top-level key (
{ <ComponentName>: schema }), butkeys[0]!silently picks an arbitrary key if more than one is present (e.g. if the extractor ever adds metadata or someone hand-merges manifests). A check would surface the mistake atdefineCatalogtime rather than producing a wrong protocol name.♻️ Proposed tightening
function isTuple( input: CatalogInput, ): input is readonly [CatalogComponent, CatalogManifest] { - return Array.isArray(input); + return Array.isArray(input) && input.length === 2; } @@ if (isTuple(input)) { const [component, manifest] = input; + if (typeof manifest !== 'object' || manifest === null) { + throw new Error( + '[a2ui] Tuple input expects `[component, manifest]`; ' + + 'received a non-object manifest.', + ); + } const keys = Object.keys(manifest); if (keys.length === 0) { throw new Error( '[a2ui] Empty manifest passed to defineCatalog; expected ' + '`{ <ComponentName>: schema }`.', ); } + if (keys.length > 1) { + throw new Error( + `[a2ui] Manifest passed to defineCatalog has multiple top-level ` + + `keys (${keys.join(', ')}); expected exactly one ` + + `\`{ <ComponentName>: schema }\`.`, + ); + } const name = keys[0]!;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/genui/a2ui/src/catalog/defineCatalog.ts` around lines 72 - 112, The tuple-detection and tuple-handling are too permissive: tighten isTuple to only return true for a two-element array whose second element is a non-null object (so malformed inputs like [] or [Component] aren’t treated as tuples), and in resolveInput avoid destructuring before validation and explicitly validate the manifest object and its keys count — throw a clear error if the manifest has no keys or if it has more than one key (don’t silently pick keys[0]). Update isTuple and resolveInput (referencing isTuple, resolveInput and deriveBareName) to perform these checks and emit the friendly “[a2ui] …” errors for empty/multi-key manifests instead of runtime TypeErrors or silent wrong-name selection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/genui/a2ui/src/catalog/defineCatalog.ts`:
- Around line 72-112: The tuple-detection and tuple-handling are too permissive:
tighten isTuple to only return true for a two-element array whose second element
is a non-null object (so malformed inputs like [] or [Component] aren’t treated
as tuples), and in resolveInput avoid destructuring before validation and
explicitly validate the manifest object and its keys count — throw a clear error
if the manifest has no keys or if it has more than one key (don’t silently pick
keys[0]). Update isTuple and resolveInput (referencing isTuple, resolveInput and
deriveBareName) to perform these checks and emit the friendly “[a2ui] …” errors
for empty/multi-key manifests instead of runtime TypeErrors or silent wrong-name
selection.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f5a7a1be-f6fc-43d5-be49-f4549dda4d3d
📒 Files selected for processing (4)
packages/genui/a2ui-catalog-extractor/src/cli.tspackages/genui/a2ui/src/catalog/README.mdpackages/genui/a2ui/src/catalog/defineCatalog.tspackages/genui/a2ui/src/catalog/index.ts
Move the catalog `<Name>/index.tsx` files (and `defineCatalog.ts`) to import from `store/types` and `react/A2UIRenderer` so they sit on the post-refactor module layout. Add three thin compatibility re-exports so the new paths resolve against the still-present `core/` implementation: - `src/store/types.ts` → re-exports `core/types.js` - `src/react/A2UIRenderer.tsx` → re-exports `A2UIRender as A2UIRenderer` and `NodeRenderer` from `core/A2UIRender.jsx` - `src/react/useDataBinding.ts` → re-exports `core/useDataBinding.js` The full headless-renderer refactor (lynx-family#2536) replaces these shims with real implementations; this PR only relocates the catalog import surface so that follow-up rebases shrink to a no-op delete of the shims. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Merging this PR will improve performance by 18.85%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | 008-many-use-state-destroyBackground |
9.5 ms | 8 ms | +18.85% |
| ⚡ | transform 1000 view elements |
47 ms | 39.9 ms | +17.72% |
Comparing PupilTong:claude/a2ui-catalog-extractor (929deed) with main (0c6e660)
Footnotes
-
26 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
Web Explorer#9368 Bundle Size — 900.03KiB (0%).929deed(current) vs 0c6e660 main#9366(baseline) Bundle metrics
|
| Current #9368 |
Baseline #9366 |
|
|---|---|---|
44.46KiB |
44.46KiB |
|
2.22KiB |
2.22KiB |
|
0% |
0% |
|
9 |
9 |
|
11 |
11 |
|
229 |
229 |
|
11 |
11 |
|
27.28% |
27.28% |
|
10 |
10 |
|
0 |
0 |
Bundle size by type no changes
| Current #9368 |
Baseline #9366 |
|
|---|---|---|
495.9KiB |
495.9KiB |
|
401.92KiB |
401.92KiB |
|
2.22KiB |
2.22KiB |
Bundle analysis report Branch PupilTong:claude/a2ui-catalog-ex... Project dashboard
Generated by RelativeCI Documentation Report issue
React Example with Element Template#62 Bundle Size — 198.12KiB (0%).929deed(current) vs 0c6e660 main#60(baseline) Bundle metrics
Bundle size by type
|
| Current #62 |
Baseline #60 |
|
|---|---|---|
145.76KiB |
145.76KiB |
|
52.36KiB |
52.36KiB |
Bundle analysis report Branch PupilTong:claude/a2ui-catalog-ex... Project dashboard
Generated by RelativeCI Documentation Report issue
React MTF Example#927 Bundle Size — 196.68KiB (0%).929deed(current) vs 0c6e660 main#925(baseline) Bundle metrics
|
| Current #927 |
Baseline #925 |
|
|---|---|---|
0B |
0B |
|
0B |
0B |
|
0% |
0% |
|
0 |
0 |
|
3 |
3 |
|
174 |
174 |
|
66 |
66 |
|
44.05% |
44.05% |
|
2 |
2 |
|
0 |
0 |
Bundle size by type no changes
| Current #927 |
Baseline #925 |
|
|---|---|---|
111.23KiB |
111.23KiB |
|
85.45KiB |
85.45KiB |
Bundle analysis report Branch PupilTong:claude/a2ui-catalog-ex... Project dashboard
Generated by RelativeCI Documentation Report issue
React Example#7795 Bundle Size — 225.52KiB (0%).929deed(current) vs 0c6e660 main#7793(baseline) Bundle metrics
|
| Current #7795 |
Baseline #7793 |
|
|---|---|---|
0B |
0B |
|
0B |
0B |
|
0% |
0% |
|
0 |
0 |
|
4 |
4 |
|
180 |
180 |
|
69 |
69 |
|
44.54% |
44.54% |
|
2 |
2 |
|
0 |
0 |
Bundle size by type no changes
| Current #7795 |
Baseline #7793 |
|
|---|---|---|
145.76KiB |
145.76KiB |
|
79.77KiB |
79.77KiB |
Bundle analysis report Branch PupilTong:claude/a2ui-catalog-ex... Project dashboard
Generated by RelativeCI Documentation Report issue
React External#910 Bundle Size — 680.82KiB (0%).929deed(current) vs 0c6e660 main#907(baseline) Bundle metrics
|
| Current #910 |
Baseline #907 |
|
|---|---|---|
0B |
0B |
|
0B |
0B |
|
0% |
0% |
|
0 |
0 |
|
3 |
3 |
|
17 |
17 |
|
5 |
5 |
|
8.59% |
8.59% |
|
0 |
0 |
|
0 |
0 |
Bundle analysis report Branch PupilTong:claude/a2ui-catalog-ex... Project dashboard
Generated by RelativeCI Documentation Report issue
Reshape `@lynx-js/a2ui-reactlynx` so the renderer, the protocol message buffer, and the catalog are independently composable. - Renderer (`src/react/`) is headless: `<A2UI>` / `<A2UIRenderer>` ship no styles or chrome, and consumers wrap surfaces themselves via `wrapSurface`. The previous `core/A2UIRender` + global `componentRegistry` path is removed. - Message buffer (`src/store/MessageStore.ts`) is a dumb append-only buffer. The developer's IO module pushes raw v0.9 protocol messages into it; `<A2UI>` subscribes via `useSyncExternalStore`, owns its own `MessageProcessor`, and processes new tail messages each render. - Catalog API (`src/catalog/defineCatalog.ts`, landed earlier in lynx-family#2560) is now the only registration path; the side-effect-based `componentRegistry` is gone, and built-ins re-export from `<Name>/index.tsx` with a paired `catalog.json` manifest. - Examples relocated under `a2ui-playground/examples/` (chat shell, mock IO, SSE IO) so they're discoverable without bloating the package surface. - web-core: stop redeclaring `fetch` as a chunk-scope binding so BTS chunks reuse `window.fetch` (needed by the playground IO module). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reshape `@lynx-js/a2ui-reactlynx` so the renderer, the protocol message buffer, and the catalog are independently composable. - Renderer (`src/react/`) is headless: `<A2UI>` / `<A2UIRenderer>` ship no styles or chrome, and consumers wrap surfaces themselves via `wrapSurface`. The previous `core/A2UIRender` + global `componentRegistry` path is removed. - Message buffer (`src/store/MessageStore.ts`) is a dumb append-only buffer. The developer's IO module pushes raw v0.9 protocol messages into it; `<A2UI>` subscribes via `useSyncExternalStore`, owns its own `MessageProcessor`, and processes new tail messages each render. - Catalog API (`src/catalog/defineCatalog.ts`, landed earlier in lynx-family#2560) is now the only registration path; the side-effect-based `componentRegistry` is gone, and built-ins re-export from `<Name>/index.tsx` with a paired `catalog.json` manifest. - Examples relocated under `a2ui-playground/examples/` (chat shell, mock IO, SSE IO) so they're discoverable without bloating the package surface. - web-core: stop redeclaring `fetch` as a chunk-scope binding so BTS chunks reuse `window.fetch` (needed by the playground IO module). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reshape `@lynx-js/a2ui-reactlynx` so the renderer, the protocol message buffer, and the catalog are independently composable. - Renderer (`src/react/`) is headless: `<A2UI>` / `<A2UIRenderer>` ship no styles or chrome, and consumers wrap surfaces themselves via `wrapSurface`. The previous `core/A2UIRender` + global `componentRegistry` path is removed. - Message buffer (`src/store/MessageStore.ts`) is a dumb append-only buffer. The developer's IO module pushes raw v0.9 protocol messages into it; `<A2UI>` subscribes via `useSyncExternalStore`, owns its own `MessageProcessor`, and processes new tail messages each render. - Catalog API (`src/catalog/defineCatalog.ts`, landed earlier in lynx-family#2560) is now the only registration path; the side-effect-based `componentRegistry` is gone, and built-ins re-export from `<Name>/index.tsx` with a paired `catalog.json` manifest. - Examples relocated under `a2ui-playground/examples/` (chat shell, mock IO, SSE IO) so they're discoverable without bloating the package surface. - web-core: stop redeclaring `fetch` as a chunk-scope binding so BTS chunks reuse `window.fetch` (needed by the playground IO module). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reshape `@lynx-js/a2ui-reactlynx` so the renderer, the protocol message buffer, and the catalog are independently composable. - Renderer (`src/react/`) is headless: `<A2UI>` / `<A2UIRenderer>` ship no styles or chrome, and consumers wrap surfaces themselves via `wrapSurface`. The previous `core/A2UIRender` + global `componentRegistry` path is removed. - Message buffer (`src/store/MessageStore.ts`) is a dumb append-only buffer. The developer's IO module pushes raw v0.9 protocol messages into it; `<A2UI>` subscribes via `useSyncExternalStore`, owns its own `MessageProcessor`, and processes new tail messages each render. - Catalog API (`src/catalog/defineCatalog.ts`, landed earlier in lynx-family#2560) is now the only registration path; the side-effect-based `componentRegistry` is gone, and built-ins re-export from `<Name>/index.tsx` with a paired `catalog.json` manifest. - Examples relocated under `a2ui-playground/examples/` (chat shell, mock IO, SSE IO) so they're discoverable without bloating the package surface. - web-core: stop redeclaring `fetch` as a chunk-scope binding so BTS chunks reuse `window.fetch` (needed by the playground IO module). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reshape `@lynx-js/a2ui-reactlynx` so the renderer, the protocol message buffer, and the catalog are independently composable. - Renderer (`src/react/`) is headless: `<A2UI>` / `<A2UIRenderer>` ship no styles or chrome, and consumers wrap surfaces themselves via `wrapSurface`. The previous `core/A2UIRender` + global `componentRegistry` path is removed. - Message buffer (`src/store/MessageStore.ts`) is a dumb append-only buffer. The developer's IO module pushes raw v0.9 protocol messages into it; `<A2UI>` subscribes via `useSyncExternalStore`, owns its own `MessageProcessor`, and processes new tail messages each render. - Catalog API (`src/catalog/defineCatalog.ts`, landed earlier in lynx-family#2560) is now the only registration path; the side-effect-based `componentRegistry` is gone, and built-ins re-export from `<Name>/index.tsx` with a paired `catalog.json` manifest. - Examples relocated under `a2ui-playground/examples/` (chat shell, mock IO, SSE IO) so they're discoverable without bloating the package surface. - web-core: stop redeclaring `fetch` as a chunk-scope binding so BTS chunks reuse `window.fetch` (needed by the playground IO module). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reshape `@lynx-js/a2ui-reactlynx` so the renderer, the protocol message buffer, and the catalog are independently composable. - Renderer (`src/react/`) is headless: `<A2UI>` / `<A2UIRenderer>` ship no styles or chrome, and consumers wrap surfaces themselves via `wrapSurface`. The previous `core/A2UIRender` + global `componentRegistry` path is removed. - Message buffer (`src/store/MessageStore.ts`) is a dumb append-only buffer. The developer's IO module pushes raw v0.9 protocol messages into it; `<A2UI>` subscribes via `useSyncExternalStore`, owns its own `MessageProcessor`, and processes new tail messages each render. - Catalog API (`src/catalog/defineCatalog.ts`, landed earlier in lynx-family#2560) is now the only registration path; the side-effect-based `componentRegistry` is gone, and built-ins re-export from `<Name>/index.tsx` with a paired `catalog.json` manifest. - Examples relocated under `a2ui-playground/examples/` (chat shell, mock IO, SSE IO) so they're discoverable without bloating the package surface. - web-core: stop redeclaring `fetch` as a chunk-scope binding so BTS chunks reuse `window.fetch` (needed by the playground IO module).
Reshape `@lynx-js/a2ui-reactlynx` so the renderer, the protocol message buffer, and the catalog are independently composable. - Renderer (`src/react/`) is headless: `<A2UI>` / `<A2UIRenderer>` ship no styles or chrome, and consumers wrap surfaces themselves via `wrapSurface`. The previous `core/A2UIRender` + global `componentRegistry` path is removed. - Message buffer (`src/store/MessageStore.ts`) is a dumb append-only buffer. The developer's IO module pushes raw v0.9 protocol messages into it; `<A2UI>` subscribes via `useSyncExternalStore`, owns its own `MessageProcessor`, and processes new tail messages each render. - Catalog API (`src/catalog/defineCatalog.ts`, landed earlier in lynx-family#2560) is now the only registration path; the side-effect-based `componentRegistry` is gone, and built-ins re-export from `<Name>/index.tsx` with a paired `catalog.json` manifest. - Examples relocated under `a2ui-playground/examples/` (chat shell, mock IO, SSE IO) so they're discoverable without bloating the package surface. - web-core: stop redeclaring `fetch` as a chunk-scope binding so BTS chunks reuse `window.fetch` (needed by the playground IO module).
Summary
defineCatalogAPI that lets renderer consumers compose per-instance catalogs without depending on the globalcomponentRegistryside-effect path. Bundlers tree-shake unused built-ins because the import surface is per-component.mergeCatalogs,serializeCatalog, andresolveCatalogso app shells can layer brand/page-level catalogs and emit the agent-handshake JSON (with optional per-component schemas) directly.bin/a2ui-catalog-extractor.{m,c,}jsis recognized alongside direct ESM execution.This PR is additive —
export * from './all.js'is preserved incatalog/index.ts, so existing consumers of the global registry are unaffected.Companion to the broader headless-renderer refactor in #2536; this slice can land independently because nothing in
core/orchat/depends on it.Test plan
pnpm -F @lynx-js/a2ui-reactlynx build— extractor still emitsdist/catalog/<Name>/catalog.jsonfor all 10 built-ins.pnpm -F @lynx-js/a2ui-catalog-extractor test— 6/6 extractor tests pass.pnpm -F @lynx-js/a2ui-reactlynx exec tsc --noEmit— clean (pre-existinglynx-ui-inputerror inchat/Conversation.tsxis unrelated).defineCatalog([Text])produces[{ name: 'Text', component: Text }].defineCatalog([[Text, textManifest]])reads the name from the manifest key (survives minification).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Refactor
Chores