Skip to content
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ node_modules
dist
lib
www
.github/*.instructions.md
playwright-report
test-results
trace.zip
Expand Down
236 changes: 236 additions & 0 deletions packages/genui/a2ui-catalog-extractor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
# A2UI Catalog Extractor

`@lynx-js/a2ui-catalog-extractor` generates A2UI catalog schemas from explicit declaration syntax plus standard JSDoc, TSDoc, and TypeDoc metadata.

It is designed for the cases where checker-driven type extraction becomes brittle. The extractor reads the shapes that authors write directly in `.tsx`, keeps `.jsx` support as a best-effort path, and can emit either:

- legacy per-component shards such as `dist/catalog/Button/catalog.json`
- a full catalog object with `catalogId`, `components`, and optional root metadata passthrough

## What It Supports

The extractor reads:

- exported component function names as catalog component keys
- explicit TypeScript syntax for:
- primitives
- string literal unions
- arrays
- object literals and interfaces
- optional properties
- local aliases
- `Record<string, T>` and string index signatures
- unions such as `string | { path: string }`
- standard documentation tags for:
- property `description`
- `default`
- `deprecated`
- named local interfaces and type aliases for complex nested object graphs
- `.jsx` best-effort typedef parsing through `@typedef`, `@property`, and parameter or property JSDoc type expressions

The legacy compatibility output covers the schema fields currently emitted by A2UI:

- `properties`
- `required`
- property `description`
- property `type`
- property `enum`
- property `oneOf`
- property `items`
- nested `properties`
- `additionalProperties`

## Authoring Model

Use `.tsx` as the primary authoring path.

```tsx
type Binding = { path: string };
type BindableText = string | Binding;

export interface TextProps {
/** Literal text or a binding path. */
text: BindableText;

/**
* Visual tone.
* @defaultValue "body"
*/
tone?: 'body' | 'caption';
}

export function Text(_props: TextProps): null {
return null;
}
```

That produces a schema shaped like:

```json
{
"Text": {
"properties": {
"text": {
"description": "Literal text or a binding path.",
"oneOf": [
{ "type": "string" },
{
"type": "object",
"properties": {
"path": { "type": "string" }
},
"required": ["path"],
"additionalProperties": false
}
]
},
"tone": {
"description": "Visual tone.",
"type": "string",
"enum": ["body", "caption"],
"default": "body"
}
},
"required": ["text"]
}
}
```

## Standard Tags

Prefer standard tags first:

| Source | Generated field |
| ---------------------------- | ------------------------- |
| summary text | `description` |
| `@remarks` | appended to `description` |
| `@defaultValue` / `@default` | `default` |
| `@deprecated` | `deprecated` |
| string literal union | `enum` |
| optional property | omitted from `required` |

For more complex schemas, keep the structure explicit in local types instead of relying on custom tags.

```ts
export interface ActionContextBinding {
path: string;
}

export type ActionContextValue =
| string
| number
| boolean
| ActionContextBinding;

export interface ActionEvent {
name: string;
/** Context is a JSON object map in v0.9. */
context?: Record<string, ActionContextValue>;
}

export interface ActionPayload {
event: ActionEvent;
}

export interface ButtonProps {
/** Host action payload. */
action: ActionPayload;
}
```

See [references/tsdoc-mapping.md](./references/tsdoc-mapping.md) for the full mapping contract.

## CLI

Generate legacy shards:

```bash
a2ui-catalog-extractor generate \
--source ./src/catalog \
--out ./dist/catalog \
--tsconfig ./tsconfig.json \
--format legacy-shards
```

Check generated output:

```bash
a2ui-catalog-extractor check \
--source ./src/catalog \
--out ./dist/catalog \
--tsconfig ./tsconfig.json \
--format legacy-shards
```

Generate a full catalog object:

```bash
a2ui-catalog-extractor generate \
--source ./src/catalog \
--out ./dist/catalog \
--tsconfig ./tsconfig.json \
--format a2ui-catalog \
--catalog-id demo-catalog \
--title "Demo Catalog"
```

## API

```ts
import {
extractCatalog,
writeCatalogFiles,
} from '@lynx-js/a2ui-catalog-extractor';

const result = await extractCatalog({
sourceDir: './src/catalog',
tsconfigPath: './tsconfig.json',
format: 'legacy-shards',
});

await writeCatalogFiles(result, {
outDir: './dist/catalog',
});
```

## JSX Support

`.jsx` is best-effort in v1. Prefer a typedef block that fully describes the component props:

```jsx
/**
* @typedef {object} BadgeProps
* @property {string | { path: string }} text Literal badge text.
* @property {'info' | 'warning'} [tone] Badge tone.
*/

/**
* @param {BadgeProps} props
*/
export function Badge(props) {
return props;
}
```

Complex nested schemas in `.jsx` should move to `.tsx` so the structure can stay explicit.

## A2UI Integration

The A2UI package uses this extractor during its build:

```bash
node --experimental-strip-types ../a2ui-catalog-extractor/src/cli.ts generate ...
```

This keeps the catalog build independent from a prebuilt extractor package while still preserving a normal ESM library and CLI build for direct package consumption.

## Validation

Run the focused package checks with Node 24 in this repository:

```bash
fnm exec --using v24.15.0 -- pnpm --filter @lynx-js/a2ui-catalog-extractor build
fnm exec --using v24.15.0 -- pnpm --filter @lynx-js/a2ui-catalog-extractor test
```

The test suite includes golden parity coverage for the current A2UI legacy catalog shards.
46 changes: 46 additions & 0 deletions packages/genui/a2ui-catalog-extractor/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
name: a2ui-catalog-extractor
description: Use when updating the A2UI catalog extractor, annotating catalog components with JSDoc or TSDoc, or validating legacy catalog shard parity.
---

# A2UI Catalog Extractor

Use this skill when you are:

- changing `packages/genui/a2ui-catalog-extractor`
- annotating `packages/genui/a2ui/src/catalog/*/index.tsx`
- debugging generated `dist/catalog/*/catalog.json` output

## Workflow

1. Keep `.tsx` as the primary authoring path.
2. Prefer explicit declaration syntax and standard tags over custom annotations.
3. Model complex nested schemas with named local interfaces and type aliases.
4. Preserve legacy shard compatibility for A2UI unless the task explicitly changes the contract.

## Key Rules

- Component names come from exported symbols.
- Property descriptions come from normal doc comments.
- `@defaultValue` and `@default` map to JSON Schema `default`.
- String literal unions map to `enum`.
- Optional props are omitted from `required`.
- Ignore framework-only props such as `id`, `surface`, `setValue`, `sendAction`, `dataContextPath`, `__template`, and `component`.

## Important Implementation Note

TypeDoc is the primary source for standard documentation metadata. The extractor should rely on explicit local TypeScript syntax for schema structure instead of custom schema tags.

## References

- Read [references/tsdoc-mapping.md](./references/tsdoc-mapping.md) when changing extraction rules.
- Read [references/a2ui-v0.9-schema.md](./references/a2ui-v0.9-schema.md) when changing the catalog surface or compatibility targets.

## Validation

Use the repository's Node 24 toolchain:

```bash
fnm exec --using v24.15.0 -- pnpm --filter @lynx-js/a2ui-catalog-extractor test
fnm exec --using v24.15.0 -- pnpm --filter @lynx-js/a2ui-reactlynx build
```
7 changes: 7 additions & 0 deletions packages/genui/a2ui-catalog-extractor/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface:
display_name: "A2UI Catalog Extractor"
short_description: "Update A2UI catalog extraction and docs."
default_prompt: "Use $a2ui-catalog-extractor to update the A2UI catalog extractor or catalog component annotations."

policy:
allow_implicit_invocation: true
66 changes: 66 additions & 0 deletions packages/genui/a2ui-catalog-extractor/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2026 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.

import eslint from '@eslint/js';
import { defineConfig, globalIgnores } from 'eslint/config';
import headers from 'eslint-plugin-headers';
import tseslint from 'typescript-eslint';

export default defineConfig(
globalIgnores([
'.rslib/**',
'dist/**',
'node_modules/**',
'rslib.config.ts',
'rstest.config.ts',
'test/fixtures/**',
]),
eslint.configs.recommended,
tseslint.configs.recommended,
{
languageOptions: {
parserOptions: {
tsconfigRootDir: import.meta.dirname,
projectService: {
allowDefaultProject: ['eslint.config.js'],
defaultProject: './tsconfig.json',
},
},
},
},
{
plugins: {
headers,
},
rules: {
'headers/header-format': [
'error',
{
source: 'string',
style: 'line',
content: [
'Copyright (year) {authors}. All rights reserved.',
'Licensed under the (license) that can be found in the',
'LICENSE file in the root directory of this source tree.',
].join('\n'),
variables: {
authors: 'The Lynx Authors',
},
patterns: {
year: {
pattern: '\\d{4}',
defaultValue: new Date().getFullYear().toString(),
},
license: {
pattern: [
'Apache License Version 2.0',
].join('|'),
defaultValue: 'Apache License Version 2.0',
},
},
},
],
},
},
);
Loading
Loading