From 6ec849649e28a37dd8d25d29d21e1514b028d95d Mon Sep 17 00:00:00 2001 From: Haoyang Wang <12288479+PupilTong@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:48:50 +0800 Subject: [PATCH 1/5] docs: expand a2ui catalog extractor guide --- .../genui/a2ui-catalog-extractor/AGENTS.md | 14 + .../genui/a2ui-catalog-extractor/README.md | 624 +++++++++++++++-- .../a2ui-catalog-extractor/readme.zh_cn.md | 626 ++++++++++++++++++ .../test/extractor.test.ts | 110 +++ .../test/fixtures/catalog/QuickStartCard.tsx | 39 ++ .../QuickStartCard/catalog.json | 83 +++ 6 files changed, 1437 insertions(+), 59 deletions(-) create mode 100644 packages/genui/a2ui-catalog-extractor/AGENTS.md create mode 100644 packages/genui/a2ui-catalog-extractor/readme.zh_cn.md create mode 100644 packages/genui/a2ui-catalog-extractor/test/fixtures/catalog/QuickStartCard.tsx create mode 100644 packages/genui/a2ui-catalog-extractor/test/fixtures/expected-catalog/QuickStartCard/catalog.json diff --git a/packages/genui/a2ui-catalog-extractor/AGENTS.md b/packages/genui/a2ui-catalog-extractor/AGENTS.md new file mode 100644 index 0000000000..b64f6f1bcb --- /dev/null +++ b/packages/genui/a2ui-catalog-extractor/AGENTS.md @@ -0,0 +1,14 @@ +# a2ui-catalog-extractor + +When editing this package's user documentation, update both `README.md` +and `readme.zh_cn.md`. The English README is the primary document and the +Chinese README should remain an equivalent translation, not a shorter +summary. + +Keep README examples tied to tests. If a documented component contract, +generated schema, CLI flow, or API example changes, update or add a test +fixture or test case in this package so the documented behavior is checked. + +This package is currently `private` in the monorepo. Documentation may +describe npm usage for a future published package, but it must clearly +separate published-package usage from current workspace usage. diff --git a/packages/genui/a2ui-catalog-extractor/README.md b/packages/genui/a2ui-catalog-extractor/README.md index 2b5122f0a1..3709e9ebdc 100644 --- a/packages/genui/a2ui-catalog-extractor/README.md +++ b/packages/genui/a2ui-catalog-extractor/README.md @@ -1,129 +1,635 @@ # A2UI Catalog Extractor -`@lynx-js/a2ui-catalog-extractor` generates A2UI component catalog JSON from TypeDoc project reflections. -Developers author catalog-facing TypeScript interfaces and comments; this package consumes TypeDoc reflection data and writes `catalog.json`. +English | [简体中文](./readme.zh_cn.md) -The extractor does not parse TS/TSX source text, does not import the TypeScript compiler API, and does not ask developers to write JSON Schema. +`@lynx-js/a2ui-catalog-extractor` turns TypeScript component +interfaces into A2UI component catalog JSON. You write the public +component contract once as a TypeScript `interface`, describe it with +normal TypeDoc comments, and let this package generate the JSON Schema +that an A2UI agent can read. -## A2UI Catalog Shape +This package is currently `private` inside the Lynx Stack monorepo. The +npm installation examples below describe how the package should be used +after it is published. When working in this repository, use the workspace +commands shown in [Using It In This Monorepo](#using-it-in-this-monorepo). -A2UI v0.9 catalogs describe the capabilities a renderer exposes to an agent: +## What It Does -- `catalogId`: stable catalog identifier used during catalog negotiation. -- `components`: component name to JSON Schema for runtime component props. -- `functions`: named functions with JSON Schema `parameters` and a scalar `returnType`. -- `theme`: theme property name to JSON Schema. +A2UI catalogs describe what components a renderer supports. For each +component, the catalog tells an agent which props are valid, which props +are required, which enum values are allowed, and what each field means. -This package generates the `components` map and can wrap it with `catalogId`, `functions`, and `theme` through `createA2UICatalog`. +This extractor generates the `components` part of an A2UI v0.9 catalog: -## Authoring Rules +```json +{ + "QuickStartCard": { + "properties": { + "title": { "type": "string" } + }, + "required": ["title"] + } +} +``` + +It can also wrap those generated components with a `catalogId`, +`functions`, and `theme` through `createA2UICatalog`. + +## What It Does Not Do + +- It does not render A2UI UI. +- It does not parse TypeScript source text by hand. +- It does not use the TypeScript compiler API directly. +- It does not ask you to write JSON Schema in comments. +- It does not expand arbitrary imported type aliases or external + interfaces. + +The package consumes TypeDoc reflection data. This keeps the implementation +small, but it also means catalog-facing shapes should be written inline in +the marked interface. + +## Requirements + +- Node.js 22 or newer. +- TypeScript or TSX source files that TypeDoc can read. +- One TypeScript `interface` per catalog-facing component contract. + +## Installation -Only TypeScript `interface` reflections are converted. -Mark the catalog-facing interface with the single custom tag: +### Published npm package + +After this package is published, install it as a development dependency: + +```bash +pnpm add -D @lynx-js/a2ui-catalog-extractor +``` + +Then add a script to your package: + +```json +{ + "scripts": { + "build:catalog": "a2ui-catalog-extractor --catalog-dir src/catalog --out-dir dist/catalog" + } +} +``` + +Run it with: + +```bash +pnpm build:catalog +``` + +### Using It In This Monorepo + +The package is already wired into `@lynx-js/a2ui-reactlynx`: + +```bash +pnpm -C packages/genui/a2ui build +``` + +That command runs: + +```bash +a2ui-catalog-extractor --catalog-dir src/catalog --out-dir dist/catalog +``` + +To work on the extractor itself: + +```bash +pnpm -C packages/genui/a2ui-catalog-extractor test +``` + +If dependencies are missing, install them from the repository root first: + +```bash +pnpm install --frozen-lockfile +``` + +## Quick Start + +This example mirrors the tested fixture at +`test/fixtures/catalog/QuickStartCard.tsx`. + +### 1. Create a catalog-facing interface + +Create `src/catalog/QuickStartCard.tsx`: ```tsx /** - * @a2uiCatalog Text + * Quick start card fixture. + * + * @remarks This fixture mirrors the README quick start. + * @a2uiCatalog QuickStartCard */ -export interface TextProps { - /** Literal text or path binding. */ - text: string | { path: string }; - variant?: 'h1' | 'h2' | 'body'; +export interface QuickStartCardProps { + /** Card title text or data binding. */ + title: string | { path: string }; + /** Visual tone used by the renderer. */ + tone?: 'neutral' | 'accent'; + /** + * Tags shown below the title. + * + * @defaultValue `[]` + */ + tags?: string[]; + /** Author metadata rendered in the card footer. */ + author: { + /** Display name. */ + name: string; + /** Optional profile URL. */ + url?: string; + }; + /** + * Extra analytics context sent with user actions. + * + * @defaultValue `{}` + */ + context?: Record; } ``` -The generated schema is: +The important part is `@a2uiCatalog QuickStartCard`. It tells the +extractor that this interface should become a catalog component named +`QuickStartCard`. + +### 2. Generate catalog files + +Run: + +```bash +a2ui-catalog-extractor --catalog-dir src/catalog --out-dir dist/catalog +``` + +The extractor scans the catalog directory, finds interfaces marked with +`@a2uiCatalog`, and writes one file per component: + +```text +dist/catalog/ + QuickStartCard/ + catalog.json +``` + +### 3. Read the generated schema + +`dist/catalog/QuickStartCard/catalog.json` will look like this: ```json { - "Text": { + "QuickStartCard": { "properties": { - "text": { + "title": { "oneOf": [ - { "type": "string" }, + { + "type": "string" + }, { "type": "object", - "properties": { "path": { "type": "string" } }, - "required": ["path"], + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ], "additionalProperties": false } ], - "description": "Literal text or path binding." + "description": "Card title text or data binding." }, - "variant": { + "tone": { "type": "string", - "enum": ["h1", "h2", "body"] + "enum": [ + "neutral", + "accent" + ], + "description": "Visual tone used by the renderer." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags shown below the title.", + "default": [] + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Display name." + }, + "url": { + "type": "string", + "description": "Optional profile URL." + } + }, + "required": [ + "name" + ], + "additionalProperties": false, + "description": "Author metadata rendered in the card footer." + }, + "context": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "description": "Extra analytics context sent with user actions.", + "default": {} } }, - "required": ["text"] + "required": [ + "title", + "author" + ], + "description": "Quick start card fixture.\n\nThis fixture mirrors the README quick start." } } ``` -## Comment Mapping +Notice the main conversions: + +- `title` is required because it does not use `?`. +- `tone` becomes a string enum. +- `tags?: string[]` becomes an optional array of strings. +- `author` becomes a strict inline object with + `additionalProperties: false`. +- `context?: Record` becomes an object + map with `additionalProperties`. +- TypeDoc comments become JSON Schema descriptions. + +## Authoring Guide + +### Mark only the catalog contract + +Only TypeScript `interface` reflections are converted. Put +`@a2uiCatalog` on the interface that describes the props an agent is +allowed to send: + +```tsx +/** + * @a2uiCatalog Text + */ +export interface TextProps { + text: string; +} +``` + +Do not put the tag on the component function: + +```tsx +export function Text(_props: TextProps) { + return null; +} +``` + +### Component names + +You can write the component name explicitly: + +```tsx +/** + * @a2uiCatalog Text + */ +export interface TextProps {} +``` + +If the tag is empty, the extractor infers the name from the interface by +removing a trailing `Props` or `ComponentProps`: + +```tsx +/** + * @a2uiCatalog + */ +export interface DemoTextProps {} +``` + +This becomes `DemoText`. + +### Comments become schema metadata + +Use normal TypeDoc comments: + +```tsx +/** + * User-facing card. + * + * @remarks Use this for compact summaries. + * @a2uiCatalog SummaryCard + */ +export interface SummaryCardProps { + /** + * Optional display density. + * + * @defaultValue `"comfortable"` + */ + density?: 'compact' | 'comfortable'; +} +``` + +The extractor maps comments like this: + +| TypeDoc comment | JSON Schema output | +| ----------------------------- | -------------------------------- | +| Summary text | `description` | +| `@remarks` | Appended to `description` | +| `@defaultValue` or `@default` | `default` | +| `@deprecated` | `deprecated: true` | +| Optional property `?` | Property omitted from `required` | + +For object and array defaults, put JSON inside a code span: + +```tsx +/** + * @defaultValue `{}` + */ +context?: Record; +``` + +Without the code span, TypeDoc may pass formatted text instead of the raw +JSON value. + +### Supported TypeScript shapes -Only `@a2uiCatalog` is custom. -All other metadata uses standard TypeDoc-supported tags: +| TypeScript shape | JSON Schema shape | +| ---------------------------- | ------------------------------------------------ | +| `string` | `{ "type": "string" }` | +| `number` | `{ "type": "number" }` | +| `boolean` | `{ "type": "boolean" }` | +| `'a' \| 'b'` | `{ "type": "string", "enum": ["a", "b"] }` | +| `string \| { path: string }` | `{ "oneOf": [...] }` | +| `T[]` | `{ "type": "array", "items": ... }` | +| `Array` | `{ "type": "array", "items": ... }` | +| `ReadonlyArray` | `{ "type": "array", "items": ... }` | +| `{ name: string }` | Strict object with `additionalProperties: false` | +| `Record` | Object map with `additionalProperties: ...` | -- summary text maps to JSON Schema `description`. -- `@remarks` is appended to `description`. -- `@defaultValue` maps to JSON Schema `default`; JSON values are parsed when possible. Wrap object and array defaults in a code span, for example ``@defaultValue `{}```. -- `@deprecated` maps to JSON Schema `deprecated: true`. -- Optional properties are omitted from `required`. +### Unsupported or ambiguous types -## Type Mapping +These types intentionally fail: -The extractor generates schema from the TypeDoc type model: +- `any` +- `unknown` +- `null` +- `undefined` +- `never` +- `void` +- nullable unions such as `string | null` +- most imported aliases and referenced external interfaces +- `Record` or other non-string record keys + +Prefer explicit catalog contracts: + +```tsx +// Avoid this in catalog-facing interfaces. +type ExternalCardData = { + title: string; +}; -- `string`, `number`, `boolean` -- string literal unions as `enum` -- other unions as `oneOf` -- `T[]`, `Array`, `ReadonlyArray` -- inline object type literals -- `Record` +export interface CardProps { + data: ExternalCardData; +} +``` -Unsupported references and ambiguous catalog-facing types such as `any`, `unknown`, `never`, `void`, and nullable unions fail with an actionable error. -Inline the catalog-facing shape in the marked interface instead of relying on imported type aliases. +Write the shape inline instead: -## CLI +```tsx +export interface CardProps { + data: { + title: string; + }; +} +``` -Run TypeDoc conversion and write one file per component: +## CLI Reference ```bash -a2ui-catalog-extractor --catalog-dir src/catalog --out-dir dist/catalog +a2ui-catalog-extractor [options] +``` + +| Option | Description | Default | +| ----------------------- | ---------------------------------------------------------------------------- | -------------- | +| `--catalog-dir ` | Directory to scan for source files. Repeatable. | `src/catalog` | +| `--source ` | Source file or directory to scan. Repeatable. | None | +| `--typedoc-json ` | Read an existing TypeDoc JSON project instead of running TypeDoc conversion. | None | +| `--out-dir ` | Directory where component catalog files are written. | `dist/catalog` | +| `--version`, `-v` | Print the package version. | None | +| `--help`, `-h` | Print usage. | None | + +`--source` and `--catalog-dir` can be used together. The extractor merges +all inputs, removes duplicates, sorts them, and then runs TypeDoc. + +The scanner accepts `.ts`, `.tsx`, `.js`, `.jsx`, `.mts`, and `.cts` +files. It ignores `.d.ts`, `node_modules`, `dist`, and `.turbo`. + +## Programmatic API + +### Generate components from source files + +```ts +import { + extractCatalogComponents, + writeComponentCatalogs, +} from '@lynx-js/a2ui-catalog-extractor'; + +const components = await extractCatalogComponents({ + sourceFiles: ['src/catalog/QuickStartCard.tsx'], +}); + +await writeComponentCatalogs({ + sourceFiles: ['src/catalog/QuickStartCard.tsx'], + outDir: 'dist/catalog', +}); +``` + +Use `cwd` when paths should be resolved relative to a specific project +directory: + +```ts +await writeComponentCatalogs({ + cwd: process.cwd(), + sourceFiles: ['src/catalog/QuickStartCard.tsx'], + outDir: 'dist/catalog', +}); +``` + +Use `tsconfig` when the project needs a specific TypeScript config: + +```ts +const components = await extractCatalogComponents({ + cwd: process.cwd(), + sourceFiles: ['src/catalog/QuickStartCard.tsx'], + tsconfig: 'tsconfig.json', +}); +``` + +### Generate components from TypeDoc JSON + +If your build already produces a TypeDoc JSON project, reuse it: + +```ts +import * as fs from 'node:fs'; + +import { + extractCatalogComponentsFromTypeDocJson, + writeCatalogComponents, +} from '@lynx-js/a2ui-catalog-extractor'; + +const projectJson = JSON.parse( + await fs.promises.readFile('typedoc.json', 'utf8'), +); +const components = extractCatalogComponentsFromTypeDocJson(projectJson); + +writeCatalogComponents(components, { + outDir: 'dist/catalog', +}); ``` -Use an existing TypeDoc JSON project: +The equivalent CLI command is: ```bash a2ui-catalog-extractor --typedoc-json typedoc.json --out-dir dist/catalog ``` -## API +### Create a full A2UI catalog object + +`createA2UICatalog` is a small helper that wraps generated components with +the other top-level A2UI catalog fields: ```ts import { createA2UICatalog, extractCatalogComponents, - extractCatalogComponentsFromTypeDocJson, - writeComponentCatalogs, } from '@lynx-js/a2ui-catalog-extractor'; const components = await extractCatalogComponents({ - sourceFiles: ['src/catalog/Text/index.tsx'], + sourceFiles: ['src/catalog/QuickStartCard.tsx'], }); const catalog = createA2UICatalog({ catalogId: 'https://example.com/catalogs/basic/v1/catalog.json', components, + functions: [ + { + name: 'formatDisplayValue', + description: 'Format a raw value for display.', + parameters: { + type: 'object', + properties: { + value: { type: 'string' }, + }, + required: ['value'], + additionalProperties: false, + }, + returnType: 'string', + }, + ], + theme: { + accentColor: { type: 'string' }, + }, }); +``` -await writeComponentCatalogs({ - sourceFiles: ['src/catalog/Text/index.tsx'], - outDir: 'dist/catalog', -}); +`functions` and `theme` are not extracted from TypeScript. Pass them +explicitly if your catalog needs them. + +## Troubleshooting + +### `Unsupported ambiguous intrinsic TypeDoc type "unknown"` + +The catalog needs a concrete schema. Replace `unknown` or `any` with a +specific type: + +```tsx +// Fails. +payload: unknown; + +// Works. +payload: { + id: string; + count: number; +} +``` + +### `Unsupported nullable union` -const componentsFromJson = extractCatalogComponentsFromTypeDocJson(projectJson); +Nullable unions are not accepted: + +```tsx +// Fails. +label: string | null; +``` + +Make the property optional if it can be omitted: + +```tsx +label?: string; +``` + +Or model the state explicitly: + +```tsx +label: string | { path: string }; +``` + +### `Unsupported TypeDoc reference` + +The extractor only understands a small set of references: +`Array`, `ReadonlyArray`, and `Record`. Inline object +shapes in the catalog-facing interface instead of importing aliases. + +### My output directory is empty + +Check these points: + +- The scanned files contain an `interface`, not only a `type`. +- The interface has `@a2uiCatalog`. +- The path passed to `--catalog-dir` or `--source` exists. +- The files are not `.d.ts`. +- TypeDoc can parse the files with your `tsconfig`. + +### The generated schema does not include inherited props + +Inherited members are skipped. This is intentional because runtime-only +props such as renderer context should not be part of the agent-facing +catalog. Put every catalog-facing prop directly on the marked interface. + +### Should I hand-write JSON Schema instead? + +No. Keep the contract in TypeScript and comments. Hand-written schema tends +to drift away from component props, while this package makes the catalog a +repeatable build artifact. + +### Does this replace TypeScript type checking? + +No. TypeDoc conversion is used to read reflection data, not to validate +your full application. Continue running your normal TypeScript, lint, and +test commands. + +## Development Notes + +This package's README quick-start example is covered by tests. When you +change the example, update the fixture and expected catalog JSON at the +same time. + +Useful local checks: + +```bash +pnpm -C packages/genui/a2ui-catalog-extractor test +pnpm dprint fmt -- packages/genui/a2ui-catalog-extractor/README.md packages/genui/a2ui-catalog-extractor/readme.zh_cn.md packages/genui/a2ui-catalog-extractor/AGENTS.md packages/genui/a2ui-catalog-extractor/test/extractor.test.ts ``` ## References diff --git a/packages/genui/a2ui-catalog-extractor/readme.zh_cn.md b/packages/genui/a2ui-catalog-extractor/readme.zh_cn.md new file mode 100644 index 0000000000..38b2a0a7fe --- /dev/null +++ b/packages/genui/a2ui-catalog-extractor/readme.zh_cn.md @@ -0,0 +1,626 @@ +# A2UI Catalog Extractor + +[English](./README.md) | 简体中文 + +`@lynx-js/a2ui-catalog-extractor` 会把 TypeScript 组件接口转换成 A2UI +组件 catalog JSON。你只需要用 TypeScript `interface` 写一次组件的公开 +契约,用普通 TypeDoc 注释描述字段,然后让这个包生成 A2UI agent 可以读取的 +JSON Schema。 + +这个包目前在 Lynx Stack monorepo 中仍然是 `private`。下面的 npm 安装示例 +描述的是包发布后的用法。如果你正在本仓库里开发,请使用 +[在本 Monorepo 中使用](#在本-monorepo-中使用) 里的 workspace 命令。 + +## 它解决什么问题 + +A2UI catalog 用来描述 renderer 支持哪些组件。对每个组件,catalog 会告诉 +agent 哪些 props 合法、哪些 props 必填、哪些 enum 值可用,以及每个字段的 +含义。 + +这个 extractor 生成 A2UI v0.9 catalog 中的 `components` 部分: + +```json +{ + "QuickStartCard": { + "properties": { + "title": { "type": "string" } + }, + "required": ["title"] + } +} +``` + +它也可以通过 `createA2UICatalog` 把生成的 components 包装进带 +`catalogId`、`functions` 和 `theme` 的完整 catalog 对象。 + +## 它不做什么 + +- 它不渲染 A2UI UI。 +- 它不手写解析 TypeScript 源码文本。 +- 它不直接使用 TypeScript compiler API。 +- 它不要求你在注释里写 JSON Schema。 +- 它不会展开任意导入的 type alias 或外部 interface。 + +这个包消费 TypeDoc reflection 数据。这样实现更小,但也意味着面向 catalog +的类型形状应该直接内联写在被标记的 interface 中。 + +## 环境要求 + +- Node.js 22 或更新版本。 +- TypeDoc 可以读取的 TypeScript 或 TSX 源文件。 +- 每个 catalog 组件契约使用一个 TypeScript `interface`。 + +## 安装 + +### 已发布的 npm 包 + +包发布后,把它安装为开发依赖: + +```bash +pnpm add -D @lynx-js/a2ui-catalog-extractor +``` + +然后在你的 package 中加入脚本: + +```json +{ + "scripts": { + "build:catalog": "a2ui-catalog-extractor --catalog-dir src/catalog --out-dir dist/catalog" + } +} +``` + +运行: + +```bash +pnpm build:catalog +``` + +### 在本 Monorepo 中使用 + +`@lynx-js/a2ui-reactlynx` 已经接入了这个工具: + +```bash +pnpm -C packages/genui/a2ui build +``` + +这个命令实际会运行: + +```bash +a2ui-catalog-extractor --catalog-dir src/catalog --out-dir dist/catalog +``` + +如果要开发 extractor 本身: + +```bash +pnpm -C packages/genui/a2ui-catalog-extractor test +``` + +如果依赖缺失,先在仓库根目录安装依赖: + +```bash +pnpm install --frozen-lockfile +``` + +## 快速开始 + +这个示例对应测试夹具 `test/fixtures/catalog/QuickStartCard.tsx`。 + +### 1. 创建面向 catalog 的 interface + +创建 `src/catalog/QuickStartCard.tsx`: + +```tsx +/** + * Quick start card fixture. + * + * @remarks This fixture mirrors the README quick start. + * @a2uiCatalog QuickStartCard + */ +export interface QuickStartCardProps { + /** Card title text or data binding. */ + title: string | { path: string }; + /** Visual tone used by the renderer. */ + tone?: 'neutral' | 'accent'; + /** + * Tags shown below the title. + * + * @defaultValue `[]` + */ + tags?: string[]; + /** Author metadata rendered in the card footer. */ + author: { + /** Display name. */ + name: string; + /** Optional profile URL. */ + url?: string; + }; + /** + * Extra analytics context sent with user actions. + * + * @defaultValue `{}` + */ + context?: Record; +} +``` + +最关键的是 `@a2uiCatalog QuickStartCard`。它告诉 extractor:这个 interface +应该生成名为 `QuickStartCard` 的 catalog 组件。 + +### 2. 生成 catalog 文件 + +运行: + +```bash +a2ui-catalog-extractor --catalog-dir src/catalog --out-dir dist/catalog +``` + +extractor 会扫描 catalog 目录,找到带 `@a2uiCatalog` 的 interface,并为每个 +组件写出一个文件: + +```text +dist/catalog/ + QuickStartCard/ + catalog.json +``` + +### 3. 查看生成的 schema + +`dist/catalog/QuickStartCard/catalog.json` 会类似下面这样: + +```json +{ + "QuickStartCard": { + "properties": { + "title": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ], + "additionalProperties": false + } + ], + "description": "Card title text or data binding." + }, + "tone": { + "type": "string", + "enum": [ + "neutral", + "accent" + ], + "description": "Visual tone used by the renderer." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags shown below the title.", + "default": [] + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Display name." + }, + "url": { + "type": "string", + "description": "Optional profile URL." + } + }, + "required": [ + "name" + ], + "additionalProperties": false, + "description": "Author metadata rendered in the card footer." + }, + "context": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "description": "Extra analytics context sent with user actions.", + "default": {} + } + }, + "required": [ + "title", + "author" + ], + "description": "Quick start card fixture.\n\nThis fixture mirrors the README quick start." + } +} +``` + +注意这些转换: + +- `title` 没有 `?`,所以是必填。 +- `tone` 变成字符串 enum。 +- `tags?: string[]` 变成可选的字符串数组。 +- `author` 变成严格的内联对象,并带有 `additionalProperties: false`。 +- `context?: Record` 变成 object map,并用 + `additionalProperties` 描述值类型。 +- TypeDoc 注释变成 JSON Schema description。 + +## 编写规则 + +### 只标记 catalog 契约 + +只有 TypeScript `interface` reflection 会被转换。把 `@a2uiCatalog` 放在 +agent 被允许发送的 props interface 上: + +```tsx +/** + * @a2uiCatalog Text + */ +export interface TextProps { + text: string; +} +``` + +不要把 tag 放在组件函数上: + +```tsx +export function Text(_props: TextProps) { + return null; +} +``` + +### 组件名 + +你可以显式写组件名: + +```tsx +/** + * @a2uiCatalog Text + */ +export interface TextProps {} +``` + +如果 tag 内容为空,extractor 会从 interface 名推断组件名,规则是去掉结尾的 +`Props` 或 `ComponentProps`: + +```tsx +/** + * @a2uiCatalog + */ +export interface DemoTextProps {} +``` + +这会生成 `DemoText`。 + +### 注释会变成 schema 元数据 + +使用普通 TypeDoc 注释: + +```tsx +/** + * User-facing card. + * + * @remarks Use this for compact summaries. + * @a2uiCatalog SummaryCard + */ +export interface SummaryCardProps { + /** + * Optional display density. + * + * @defaultValue `"comfortable"` + */ + density?: 'compact' | 'comfortable'; +} +``` + +extractor 的映射规则如下: + +| TypeDoc 注释 | JSON Schema 输出 | +| ----------------------------- | -------------------- | +| summary 文本 | `description` | +| `@remarks` | 追加到 `description` | +| `@defaultValue` 或 `@default` | `default` | +| `@deprecated` | `deprecated: true` | +| 可选属性 `?` | 不放入 `required` | + +对象和数组默认值建议把 JSON 放在 code span 里: + +```tsx +/** + * @defaultValue `{}` + */ +context?: Record; +``` + +如果不用 code span,TypeDoc 可能传入格式化后的文本,而不是原始 JSON 值。 + +### 支持的 TypeScript 形状 + +| TypeScript 形状 | JSON Schema 形状 | +| ---------------------------- | -------------------------------------------- | +| `string` | `{ "type": "string" }` | +| `number` | `{ "type": "number" }` | +| `boolean` | `{ "type": "boolean" }` | +| `'a' \| 'b'` | `{ "type": "string", "enum": ["a", "b"] }` | +| `string \| { path: string }` | `{ "oneOf": [...] }` | +| `T[]` | `{ "type": "array", "items": ... }` | +| `Array` | `{ "type": "array", "items": ... }` | +| `ReadonlyArray` | `{ "type": "array", "items": ... }` | +| `{ name: string }` | 带 `additionalProperties: false` 的严格对象 | +| `Record` | 带 `additionalProperties: ...` 的 object map | + +### 不支持或含义不明确的类型 + +这些类型会故意报错: + +- `any` +- `unknown` +- `null` +- `undefined` +- `never` +- `void` +- `string | null` 这样的 nullable union +- 大多数导入的 alias 和被引用的外部 interface +- `Record` 或其他非 string record key + +建议写明确的 catalog 契约: + +```tsx +// 不建议在 catalog-facing interface 中这样写。 +type ExternalCardData = { + title: string; +}; + +export interface CardProps { + data: ExternalCardData; +} +``` + +改成内联形状: + +```tsx +export interface CardProps { + data: { + title: string; + }; +} +``` + +## CLI 参考 + +```bash +a2ui-catalog-extractor [options] +``` + +| 选项 | 说明 | 默认值 | +| ----------------------- | -------------------------------------------------------------- | -------------- | +| `--catalog-dir ` | 要扫描的源码目录。可重复。 | `src/catalog` | +| `--source ` | 要扫描的源码文件或目录。可重复。 | 无 | +| `--typedoc-json ` | 读取已有 TypeDoc JSON project,不重新运行 TypeDoc conversion。 | 无 | +| `--out-dir ` | 写出组件 catalog 文件的目录。 | `dist/catalog` | +| `--version`, `-v` | 打印包版本。 | 无 | +| `--help`, `-h` | 打印用法。 | 无 | + +`--source` 和 `--catalog-dir` 可以一起使用。extractor 会合并全部输入、去重、 +排序,然后运行 TypeDoc。 + +扫描器接受 `.ts`、`.tsx`、`.js`、`.jsx`、`.mts` 和 `.cts` 文件。它会忽略 +`.d.ts`、`node_modules`、`dist` 和 `.turbo`。 + +## 编程 API + +### 从源码文件生成 components + +```ts +import { + extractCatalogComponents, + writeComponentCatalogs, +} from '@lynx-js/a2ui-catalog-extractor'; + +const components = await extractCatalogComponents({ + sourceFiles: ['src/catalog/QuickStartCard.tsx'], +}); + +await writeComponentCatalogs({ + sourceFiles: ['src/catalog/QuickStartCard.tsx'], + outDir: 'dist/catalog', +}); +``` + +如果路径需要相对某个项目目录解析,使用 `cwd`: + +```ts +await writeComponentCatalogs({ + cwd: process.cwd(), + sourceFiles: ['src/catalog/QuickStartCard.tsx'], + outDir: 'dist/catalog', +}); +``` + +如果项目需要指定 TypeScript 配置,使用 `tsconfig`: + +```ts +const components = await extractCatalogComponents({ + cwd: process.cwd(), + sourceFiles: ['src/catalog/QuickStartCard.tsx'], + tsconfig: 'tsconfig.json', +}); +``` + +### 从 TypeDoc JSON 生成 components + +如果你的构建流程已经生成 TypeDoc JSON project,可以直接复用: + +```ts +import * as fs from 'node:fs'; + +import { + extractCatalogComponentsFromTypeDocJson, + writeCatalogComponents, +} from '@lynx-js/a2ui-catalog-extractor'; + +const projectJson = JSON.parse( + await fs.promises.readFile('typedoc.json', 'utf8'), +); +const components = extractCatalogComponentsFromTypeDocJson(projectJson); + +writeCatalogComponents(components, { + outDir: 'dist/catalog', +}); +``` + +等价的 CLI 命令是: + +```bash +a2ui-catalog-extractor --typedoc-json typedoc.json --out-dir dist/catalog +``` + +### 创建完整 A2UI catalog 对象 + +`createA2UICatalog` 是一个小 helper,用来把生成的 components 包装进其他 +A2UI catalog 顶层字段: + +```ts +import { + createA2UICatalog, + extractCatalogComponents, +} from '@lynx-js/a2ui-catalog-extractor'; + +const components = await extractCatalogComponents({ + sourceFiles: ['src/catalog/QuickStartCard.tsx'], +}); + +const catalog = createA2UICatalog({ + catalogId: 'https://example.com/catalogs/basic/v1/catalog.json', + components, + functions: [ + { + name: 'formatDisplayValue', + description: 'Format a raw value for display.', + parameters: { + type: 'object', + properties: { + value: { type: 'string' }, + }, + required: ['value'], + additionalProperties: false, + }, + returnType: 'string', + }, + ], + theme: { + accentColor: { type: 'string' }, + }, +}); +``` + +`functions` 和 `theme` 不会从 TypeScript 自动提取。如果 catalog 需要这些 +字段,请显式传入。 + +## 故障排查和 FAQ + +### `Unsupported ambiguous intrinsic TypeDoc type "unknown"` + +catalog 需要明确 schema。把 `unknown` 或 `any` 改成具体类型: + +```tsx +// 会失败。 +payload: unknown; + +// 可以工作。 +payload: { + id: string; + count: number; +} +``` + +### `Unsupported nullable union` + +nullable union 不被接受: + +```tsx +// 会失败。 +label: string | null; +``` + +如果字段可以省略,把它设为可选: + +```tsx +label?: string; +``` + +或者显式建模状态: + +```tsx +label: string | { path: string }; +``` + +### `Unsupported TypeDoc reference` + +extractor 只理解少量 reference:`Array`、`ReadonlyArray` 和 +`Record`。请在 catalog-facing interface 中内联对象形状,不要导入 +alias。 + +### 输出目录为空 + +检查这些点: + +- 被扫描文件里有 `interface`,而不只是 `type`。 +- interface 带有 `@a2uiCatalog`。 +- 传给 `--catalog-dir` 或 `--source` 的路径存在。 +- 文件不是 `.d.ts`。 +- TypeDoc 可以用你的 `tsconfig` 解析这些文件。 + +### 生成的 schema 为什么没有继承来的 props + +继承成员会被跳过。这是有意设计,因为 renderer context 这类运行时字段不应该 +成为 agent-facing catalog 的一部分。请把所有面向 catalog 的 props 直接写在被 +标记的 interface 上。 + +### 我应该手写 JSON Schema 吗 + +不应该。请把契约保留在 TypeScript 和注释里。手写 schema 很容易和组件 props +漂移,而这个包会让 catalog 成为可重复生成的构建产物。 + +### 这能替代 TypeScript 类型检查吗 + +不能。TypeDoc conversion 只是用来读取 reflection 数据,不是用来验证完整应用。 +请继续运行正常的 TypeScript、lint 和测试命令。 + +## 开发备注 + +这个包 README 中的 quick-start 示例有测试覆盖。修改示例时,请同时更新 fixture +和 expected catalog JSON。 + +常用本地检查: + +```bash +pnpm -C packages/genui/a2ui-catalog-extractor test +pnpm dprint fmt -- packages/genui/a2ui-catalog-extractor/README.md packages/genui/a2ui-catalog-extractor/readme.zh_cn.md packages/genui/a2ui-catalog-extractor/AGENTS.md packages/genui/a2ui-catalog-extractor/test/extractor.test.ts +``` + +## 参考资料 + +- [A2UI Catalogs](https://a2ui.org/concepts/catalogs/) +- [A2UI v0.9 protocol](https://a2ui.org/specification/v0.9-a2ui/) +- [TypeDoc custom tags](https://typedoc.org/documents/Tags.html) +- [TypeDoc JSON output](https://typedoc.org/documents/Options.Output.html) diff --git a/packages/genui/a2ui-catalog-extractor/test/extractor.test.ts b/packages/genui/a2ui-catalog-extractor/test/extractor.test.ts index 98bcf684a3..33564af941 100644 --- a/packages/genui/a2ui-catalog-extractor/test/extractor.test.ts +++ b/packages/genui/a2ui-catalog-extractor/test/extractor.test.ts @@ -8,12 +8,14 @@ import { fileURLToPath } from 'node:url'; import { afterAll, describe, expect, test } from '@rstest/core'; +import { runCli } from '../src/cli.js'; import { createA2UICatalog, extractCatalogComponents, findCatalogSourceFiles, writeComponentCatalogs, } from '../src/index.js'; +import type { TypeDocProject } from '../src/index.js'; const __filename = fileURLToPath(import.meta.url); const packageDir = path.resolve(path.dirname(__filename), '..'); @@ -36,6 +38,7 @@ describe('extractCatalogComponents', () => { expect(sourceFiles.map(file => path.basename(file))).toEqual([ 'DemoCard.tsx', 'DemoText.tsx', + 'QuickStartCard.tsx', ]); const components = await extractCatalogComponents({ @@ -51,6 +54,7 @@ describe('extractCatalogComponents', () => { expect(Object.keys(componentsByName).sort()).toEqual([ 'DemoCard', 'DemoText', + 'QuickStartCard', ]); expect(componentsByName['DemoCard']).toMatchObject({ filePath: path.join(catalogFixtureDir, 'DemoCard.tsx'), @@ -64,6 +68,12 @@ describe('extractCatalogComponents', () => { name: 'DemoText', schema: expectedCatalogs['DemoText']!['DemoText'], }); + expect(componentsByName['QuickStartCard']).toMatchObject({ + filePath: path.join(catalogFixtureDir, 'QuickStartCard.tsx'), + interfaceName: 'QuickStartCardProps', + name: 'QuickStartCard', + schema: expectedCatalogs['QuickStartCard']!['QuickStartCard'], + }); }); test('writes catalog.json files from TSX catalog fixtures', async () => { @@ -80,6 +90,7 @@ describe('extractCatalogComponents', () => { expect(components.map(component => component.name).sort()).toEqual([ 'DemoCard', 'DemoText', + 'QuickStartCard', ]); for (const componentName of Object.keys(expectedCatalogs)) { @@ -100,11 +111,77 @@ describe('extractCatalogComponents', () => { expect(createA2UICatalog({ catalogId: 'https://example.com/catalog.json', components, + functions: [ + { + description: 'Format a raw value for display.', + name: 'formatDisplayValue', + parameters: { + type: 'object', + properties: { + value: { type: 'string' }, + }, + required: ['value'], + additionalProperties: false, + }, + returnType: 'string', + }, + ], + theme: { + accentColor: { type: 'string' }, + }, })).toEqual({ catalogId: 'https://example.com/catalog.json', components: { DemoCard: expectedCatalogs['DemoCard']!['DemoCard'], DemoText: expectedCatalogs['DemoText']!['DemoText'], + QuickStartCard: expectedCatalogs['QuickStartCard']!['QuickStartCard'], + }, + functions: [ + { + description: 'Format a raw value for display.', + name: 'formatDisplayValue', + parameters: { + type: 'object', + properties: { + value: { type: 'string' }, + }, + required: ['value'], + additionalProperties: false, + }, + returnType: 'string', + }, + ], + theme: { + accentColor: { type: 'string' }, + }, + }); + }); + + test('writes catalog files from an existing TypeDoc JSON project through the CLI', async () => { + const cwd = createTempDir(); + const typedocJsonPath = path.join(cwd, 'typedoc.json'); + fs.writeFileSync( + typedocJsonPath, + `${JSON.stringify(createCliTypeDocProjectFixture(), null, 2)}\n`, + ); + + await expect(runCli([ + '--typedoc-json', + 'typedoc.json', + '--out-dir', + 'catalog-out', + ], cwd)).resolves.toBe(0); + + expect(readCatalogJson(path.join(cwd, 'catalog-out'), 'CliBadge')).toEqual({ + CliBadge: { + properties: { + label: { + type: 'string', + description: 'Badge label.', + }, + }, + required: ['label'], + description: 'CLI badge fixture.', }, }); }); @@ -155,3 +232,36 @@ function readCatalogJson( ), ) as Record; } + +function createCliTypeDocProjectFixture(): TypeDocProject { + return { + children: [ + { + kindString: 'Interface', + name: 'CliBadgeProps', + comment: { + summary: [{ text: 'CLI badge fixture.' }], + blockTags: [ + { + tag: '@a2uiCatalog', + content: [{ text: 'CliBadge' }], + }, + ], + }, + children: [ + { + kindString: 'Property', + name: 'label', + comment: { + summary: [{ text: 'Badge label.' }], + }, + type: { + type: 'intrinsic', + name: 'string', + }, + }, + ], + }, + ], + }; +} diff --git a/packages/genui/a2ui-catalog-extractor/test/fixtures/catalog/QuickStartCard.tsx b/packages/genui/a2ui-catalog-extractor/test/fixtures/catalog/QuickStartCard.tsx new file mode 100644 index 0000000000..0fc9f4f1cd --- /dev/null +++ b/packages/genui/a2ui-catalog-extractor/test/fixtures/catalog/QuickStartCard.tsx @@ -0,0 +1,39 @@ +// 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. + +/** + * Quick start card fixture. + * + * @remarks This fixture mirrors the README quick start. + * @a2uiCatalog QuickStartCard + */ +export interface QuickStartCardProps { + /** Card title text or data binding. */ + title: string | { path: string }; + /** Visual tone used by the renderer. */ + tone?: 'neutral' | 'accent'; + /** + * Tags shown below the title. + * + * @defaultValue `[]` + */ + tags?: string[]; + /** Author metadata rendered in the card footer. */ + author: { + /** Display name. */ + name: string; + /** Optional profile URL. */ + url?: string; + }; + /** + * Extra analytics context sent with user actions. + * + * @defaultValue `{}` + */ + context?: Record; +} + +export function QuickStartCard(_props: QuickStartCardProps): null { + return null; +} diff --git a/packages/genui/a2ui-catalog-extractor/test/fixtures/expected-catalog/QuickStartCard/catalog.json b/packages/genui/a2ui-catalog-extractor/test/fixtures/expected-catalog/QuickStartCard/catalog.json new file mode 100644 index 0000000000..898e8058f9 --- /dev/null +++ b/packages/genui/a2ui-catalog-extractor/test/fixtures/expected-catalog/QuickStartCard/catalog.json @@ -0,0 +1,83 @@ +{ + "QuickStartCard": { + "properties": { + "title": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ], + "additionalProperties": false + } + ], + "description": "Card title text or data binding." + }, + "tone": { + "type": "string", + "enum": [ + "neutral", + "accent" + ], + "description": "Visual tone used by the renderer." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags shown below the title.", + "default": [] + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Display name." + }, + "url": { + "type": "string", + "description": "Optional profile URL." + } + }, + "required": [ + "name" + ], + "additionalProperties": false, + "description": "Author metadata rendered in the card footer." + }, + "context": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "description": "Extra analytics context sent with user actions.", + "default": {} + } + }, + "required": [ + "title", + "author" + ], + "description": "Quick start card fixture.\n\nThis fixture mirrors the README quick start." + } +} From 3f2c1efbbc792cfe6927fd86e5d0e7885afce4f1 Mon Sep 17 00:00:00 2001 From: Haoyang Wang <12288479+PupilTong@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:05:46 +0800 Subject: [PATCH 2/5] docs: keep a2ui extractor guide external only --- .../genui/a2ui-catalog-extractor/AGENTS.md | 6 +- .../genui/a2ui-catalog-extractor/README.md | 58 +++---------------- .../a2ui-catalog-extractor/readme.zh_cn.md | 54 ++--------------- .../test/fixtures/catalog/QuickStartCard.tsx | 4 +- .../QuickStartCard/catalog.json | 2 +- 5 files changed, 19 insertions(+), 105 deletions(-) diff --git a/packages/genui/a2ui-catalog-extractor/AGENTS.md b/packages/genui/a2ui-catalog-extractor/AGENTS.md index b64f6f1bcb..ed0096b183 100644 --- a/packages/genui/a2ui-catalog-extractor/AGENTS.md +++ b/packages/genui/a2ui-catalog-extractor/AGENTS.md @@ -9,6 +9,6 @@ Keep README examples tied to tests. If a documented component contract, generated schema, CLI flow, or API example changes, update or add a test fixture or test case in this package so the documented behavior is checked. -This package is currently `private` in the monorepo. Documentation may -describe npm usage for a future published package, but it must clearly -separate published-package usage from current workspace usage. +The user-facing READMEs are for external package users only. Do not include +monorepo setup, workspace commands, current private-package status, or +repository-maintainer workflow in either README. diff --git a/packages/genui/a2ui-catalog-extractor/README.md b/packages/genui/a2ui-catalog-extractor/README.md index 3709e9ebdc..50bf7f04e0 100644 --- a/packages/genui/a2ui-catalog-extractor/README.md +++ b/packages/genui/a2ui-catalog-extractor/README.md @@ -8,11 +8,6 @@ component contract once as a TypeScript `interface`, describe it with normal TypeDoc comments, and let this package generate the JSON Schema that an A2UI agent can read. -This package is currently `private` inside the Lynx Stack monorepo. The -npm installation examples below describe how the package should be used -after it is published. When working in this repository, use the workspace -commands shown in [Using It In This Monorepo](#using-it-in-this-monorepo). - ## What It Does A2UI catalogs describe what components a renderer supports. For each @@ -56,9 +51,9 @@ the marked interface. ## Installation -### Published npm package +### Package manager -After this package is published, install it as a development dependency: +Install it as a development dependency: ```bash pnpm add -D @lynx-js/a2ui-catalog-extractor @@ -80,36 +75,10 @@ Run it with: pnpm build:catalog ``` -### Using It In This Monorepo - -The package is already wired into `@lynx-js/a2ui-reactlynx`: - -```bash -pnpm -C packages/genui/a2ui build -``` - -That command runs: - -```bash -a2ui-catalog-extractor --catalog-dir src/catalog --out-dir dist/catalog -``` - -To work on the extractor itself: - -```bash -pnpm -C packages/genui/a2ui-catalog-extractor test -``` - -If dependencies are missing, install them from the repository root first: - -```bash -pnpm install --frozen-lockfile -``` - ## Quick Start -This example mirrors the tested fixture at -`test/fixtures/catalog/QuickStartCard.tsx`. +This example walks through a complete component contract from TypeScript +interface to generated catalog JSON. ### 1. Create a catalog-facing interface @@ -117,9 +86,9 @@ Create `src/catalog/QuickStartCard.tsx`: ```tsx /** - * Quick start card fixture. + * Quick start card. * - * @remarks This fixture mirrors the README quick start. + * @remarks Use this contract as a compact card example. * @a2uiCatalog QuickStartCard */ export interface QuickStartCardProps { @@ -255,7 +224,7 @@ dist/catalog/ "title", "author" ], - "description": "Quick start card fixture.\n\nThis fixture mirrors the README quick start." + "description": "Quick start card.\n\nUse this contract as a compact card example." } } ``` @@ -619,19 +588,6 @@ No. TypeDoc conversion is used to read reflection data, not to validate your full application. Continue running your normal TypeScript, lint, and test commands. -## Development Notes - -This package's README quick-start example is covered by tests. When you -change the example, update the fixture and expected catalog JSON at the -same time. - -Useful local checks: - -```bash -pnpm -C packages/genui/a2ui-catalog-extractor test -pnpm dprint fmt -- packages/genui/a2ui-catalog-extractor/README.md packages/genui/a2ui-catalog-extractor/readme.zh_cn.md packages/genui/a2ui-catalog-extractor/AGENTS.md packages/genui/a2ui-catalog-extractor/test/extractor.test.ts -``` - ## References - [A2UI Catalogs](https://a2ui.org/concepts/catalogs/) diff --git a/packages/genui/a2ui-catalog-extractor/readme.zh_cn.md b/packages/genui/a2ui-catalog-extractor/readme.zh_cn.md index 38b2a0a7fe..314874d32e 100644 --- a/packages/genui/a2ui-catalog-extractor/readme.zh_cn.md +++ b/packages/genui/a2ui-catalog-extractor/readme.zh_cn.md @@ -7,10 +7,6 @@ 契约,用普通 TypeDoc 注释描述字段,然后让这个包生成 A2UI agent 可以读取的 JSON Schema。 -这个包目前在 Lynx Stack monorepo 中仍然是 `private`。下面的 npm 安装示例 -描述的是包发布后的用法。如果你正在本仓库里开发,请使用 -[在本 Monorepo 中使用](#在本-monorepo-中使用) 里的 workspace 命令。 - ## 它解决什么问题 A2UI catalog 用来描述 renderer 支持哪些组件。对每个组件,catalog 会告诉 @@ -52,9 +48,9 @@ agent 哪些 props 合法、哪些 props 必填、哪些 enum 值可用,以及 ## 安装 -### 已发布的 npm 包 +### 包管理器 -包发布后,把它安装为开发依赖: +把它安装为开发依赖: ```bash pnpm add -D @lynx-js/a2ui-catalog-extractor @@ -76,35 +72,9 @@ pnpm add -D @lynx-js/a2ui-catalog-extractor pnpm build:catalog ``` -### 在本 Monorepo 中使用 - -`@lynx-js/a2ui-reactlynx` 已经接入了这个工具: - -```bash -pnpm -C packages/genui/a2ui build -``` - -这个命令实际会运行: - -```bash -a2ui-catalog-extractor --catalog-dir src/catalog --out-dir dist/catalog -``` - -如果要开发 extractor 本身: - -```bash -pnpm -C packages/genui/a2ui-catalog-extractor test -``` - -如果依赖缺失,先在仓库根目录安装依赖: - -```bash -pnpm install --frozen-lockfile -``` - ## 快速开始 -这个示例对应测试夹具 `test/fixtures/catalog/QuickStartCard.tsx`。 +这个示例会完整演示如何从 TypeScript interface 生成 catalog JSON。 ### 1. 创建面向 catalog 的 interface @@ -112,9 +82,9 @@ pnpm install --frozen-lockfile ```tsx /** - * Quick start card fixture. + * Quick start card. * - * @remarks This fixture mirrors the README quick start. + * @remarks Use this contract as a compact card example. * @a2uiCatalog QuickStartCard */ export interface QuickStartCardProps { @@ -249,7 +219,7 @@ dist/catalog/ "title", "author" ], - "description": "Quick start card fixture.\n\nThis fixture mirrors the README quick start." + "description": "Quick start card.\n\nUse this contract as a compact card example." } } ``` @@ -606,18 +576,6 @@ alias。 不能。TypeDoc conversion 只是用来读取 reflection 数据,不是用来验证完整应用。 请继续运行正常的 TypeScript、lint 和测试命令。 -## 开发备注 - -这个包 README 中的 quick-start 示例有测试覆盖。修改示例时,请同时更新 fixture -和 expected catalog JSON。 - -常用本地检查: - -```bash -pnpm -C packages/genui/a2ui-catalog-extractor test -pnpm dprint fmt -- packages/genui/a2ui-catalog-extractor/README.md packages/genui/a2ui-catalog-extractor/readme.zh_cn.md packages/genui/a2ui-catalog-extractor/AGENTS.md packages/genui/a2ui-catalog-extractor/test/extractor.test.ts -``` - ## 参考资料 - [A2UI Catalogs](https://a2ui.org/concepts/catalogs/) diff --git a/packages/genui/a2ui-catalog-extractor/test/fixtures/catalog/QuickStartCard.tsx b/packages/genui/a2ui-catalog-extractor/test/fixtures/catalog/QuickStartCard.tsx index 0fc9f4f1cd..2142943d63 100644 --- a/packages/genui/a2ui-catalog-extractor/test/fixtures/catalog/QuickStartCard.tsx +++ b/packages/genui/a2ui-catalog-extractor/test/fixtures/catalog/QuickStartCard.tsx @@ -3,9 +3,9 @@ // LICENSE file in the root directory of this source tree. /** - * Quick start card fixture. + * Quick start card. * - * @remarks This fixture mirrors the README quick start. + * @remarks Use this contract as a compact card example. * @a2uiCatalog QuickStartCard */ export interface QuickStartCardProps { diff --git a/packages/genui/a2ui-catalog-extractor/test/fixtures/expected-catalog/QuickStartCard/catalog.json b/packages/genui/a2ui-catalog-extractor/test/fixtures/expected-catalog/QuickStartCard/catalog.json index 898e8058f9..4099478a8a 100644 --- a/packages/genui/a2ui-catalog-extractor/test/fixtures/expected-catalog/QuickStartCard/catalog.json +++ b/packages/genui/a2ui-catalog-extractor/test/fixtures/expected-catalog/QuickStartCard/catalog.json @@ -78,6 +78,6 @@ "title", "author" ], - "description": "Quick start card fixture.\n\nThis fixture mirrors the README quick start." + "description": "Quick start card.\n\nUse this contract as a compact card example." } } From c88d1c02c843e9646286cd8d0969bb56a813c866 Mon Sep 17 00:00:00 2001 From: Haoyang Wang <12288479+PupilTong@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:05:57 +0800 Subject: [PATCH 3/5] docs: add genui website sidebar --- website/.gitignore | 2 + website/rspress.config.ts | 19 +++++++- website/sidebars/genui.ts | 95 +++++++++++++++++++++++++++++++++++++++ website/sidebars/index.ts | 1 + 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 website/sidebars/genui.ts diff --git a/website/.gitignore b/website/.gitignore index 2c21ea9f5a..065a53c1b1 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -3,6 +3,8 @@ docs/en/api docs/zh/api docs/en/changelog docs/zh/changelog +docs/en/genui +docs/zh/genui # api-extractor temp diff --git a/website/rspress.config.ts b/website/rspress.config.ts index fbf64b84f1..8ff8be2426 100644 --- a/website/rspress.config.ts +++ b/website/rspress.config.ts @@ -15,7 +15,11 @@ import { } from '@shikijs/transformers'; import { camelCase } from 'change-case'; -import { createAPI, createChangelogs } from './sidebars/index.js'; +import { + createAPI, + createChangelogs, + createGenUIReadmeDocs, +} from './sidebars/index.js'; const isDev = process.env['NODE_ENV'] === 'development'; @@ -399,6 +403,11 @@ const CHANGELOG_ZH = { ), }; +const GENUI = createGenUIReadmeDocs({ + repositoryRoot: join(__dirname, '..'), + websiteRoot: __dirname, +}); + const config: UserConfig = defineConfig({ root: 'docs', llms: true, @@ -532,6 +541,10 @@ const config: UserConfig = defineConfig({ '/api/rspeedy': SIDEBARS.Config, '/zh/api/rspeedy': SIDEBARS_ZH.Config, + // GenUI + '/genui/': GENUI.en, + '/zh/genui/': GENUI.zh, + // API ...(Object.fromEntries( [ @@ -740,6 +753,10 @@ const config: UserConfig = defineConfig({ text: 'REPL', link: '/repl', }, + { + text: 'GenUI', + link: '/genui/a2ui-catalog-extractor', + }, { text: 'API', items: [ diff --git a/website/sidebars/genui.ts b/website/sidebars/genui.ts new file mode 100644 index 0000000000..1bc66bdd87 --- /dev/null +++ b/website/sidebars/genui.ts @@ -0,0 +1,95 @@ +// 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 fs from 'node:fs'; +import path from 'node:path'; + +import type { + SidebarDivider, + SidebarItem, + SidebarSectionHeader, +} from '@rspress/core'; + +type GenUISidebar = (SidebarItem | SidebarSectionHeader | SidebarDivider)[]; + +export function createGenUIReadmeDocs(options: { + repositoryRoot: string; + websiteRoot: string; +}): { + en: GenUISidebar; + zh: GenUISidebar; +} { + const packageRoot = path.join( + options.repositoryRoot, + 'packages/genui/a2ui-catalog-extractor', + ); + + syncReadme({ + languageSwitch: 'English | [简体中文](/zh/genui/a2ui-catalog-extractor)', + outFile: path.join( + options.websiteRoot, + 'docs/en/genui/a2ui-catalog-extractor.md', + ), + sourceFile: path.join(packageRoot, 'README.md'), + switchPattern: /^English \| \[简体中文\]\(\.\/readme\.zh_cn\.md\)$/m, + }); + + syncReadme({ + languageSwitch: '[English](/genui/a2ui-catalog-extractor) | 简体中文', + outFile: path.join( + options.websiteRoot, + 'docs/zh/genui/a2ui-catalog-extractor.md', + ), + sourceFile: path.join(packageRoot, 'readme.zh_cn.md'), + switchPattern: /^\[English\]\(\.\/README\.md\) \| 简体中文$/m, + }); + + return { + en: [ + { + sectionHeaderText: 'GenUI', + }, + { + dividerType: 'solid', + }, + { + text: 'A2UI Catalog Extractor', + link: '/genui/a2ui-catalog-extractor', + }, + ], + zh: [ + { + sectionHeaderText: 'GenUI', + }, + { + dividerType: 'solid', + }, + { + text: 'A2UI Catalog Extractor', + link: '/zh/genui/a2ui-catalog-extractor', + }, + ], + }; +} + +function syncReadme(options: { + languageSwitch: string; + outFile: string; + sourceFile: string; + switchPattern: RegExp; +}): void { + const content = fs.readFileSync(options.sourceFile, 'utf8'); + const nextContent = content.replace( + options.switchPattern, + options.languageSwitch, + ); + + if (nextContent === content) { + throw new Error( + `Failed to rewrite language switch in ${options.sourceFile}.`, + ); + } + + fs.mkdirSync(path.dirname(options.outFile), { recursive: true }); + fs.writeFileSync(options.outFile, nextContent); +} diff --git a/website/sidebars/index.ts b/website/sidebars/index.ts index d541d68820..1ec8549a59 100644 --- a/website/sidebars/index.ts +++ b/website/sidebars/index.ts @@ -4,3 +4,4 @@ export * from './api.js'; export * from './changelog.js'; +export * from './genui.js'; From 73cf978f6644529a0649d73a311f1e937ee8dfdd Mon Sep 17 00:00:00 2001 From: Haoyang Wang <12288479+PupilTong@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:29:47 +0800 Subject: [PATCH 4/5] docs: fix genui locale links --- website/sidebars/genui.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/website/sidebars/genui.ts b/website/sidebars/genui.ts index 1bc66bdd87..d6bb899e9b 100644 --- a/website/sidebars/genui.ts +++ b/website/sidebars/genui.ts @@ -25,7 +25,8 @@ export function createGenUIReadmeDocs(options: { ); syncReadme({ - languageSwitch: 'English | [简体中文](/zh/genui/a2ui-catalog-extractor)', + languageSwitch: + 'English | 简体中文', outFile: path.join( options.websiteRoot, 'docs/en/genui/a2ui-catalog-extractor.md', @@ -35,7 +36,8 @@ export function createGenUIReadmeDocs(options: { }); syncReadme({ - languageSwitch: '[English](/genui/a2ui-catalog-extractor) | 简体中文', + languageSwitch: + 'English | 简体中文', outFile: path.join( options.websiteRoot, 'docs/zh/genui/a2ui-catalog-extractor.md', From 2deb133dea4c3b6b14bb6ba1fb2c70b521aa6127 Mon Sep 17 00:00:00 2001 From: Haoyang Wang <12288479+PupilTong@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:46:46 +0800 Subject: [PATCH 5/5] docs: move genui docs into guide --- website/.gitignore | 4 +-- website/rspress.config.ts | 14 +++------ website/sidebars/genui.ts | 64 ++++++++++++++++----------------------- 3 files changed, 32 insertions(+), 50 deletions(-) diff --git a/website/.gitignore b/website/.gitignore index 065a53c1b1..58112a5487 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -3,8 +3,8 @@ docs/en/api docs/zh/api docs/en/changelog docs/zh/changelog -docs/en/genui -docs/zh/genui +docs/en/guide/genui +docs/zh/guide/genui # api-extractor temp diff --git a/website/rspress.config.ts b/website/rspress.config.ts index 8ff8be2426..f463fa73a2 100644 --- a/website/rspress.config.ts +++ b/website/rspress.config.ts @@ -18,7 +18,7 @@ import { camelCase } from 'change-case'; import { createAPI, createChangelogs, - createGenUIReadmeDocs, + createGenUIGuideReadmeDocs, } from './sidebars/index.js'; const isDev = process.env['NODE_ENV'] === 'development'; @@ -403,7 +403,7 @@ const CHANGELOG_ZH = { ), }; -const GENUI = createGenUIReadmeDocs({ +const GENUI = createGenUIGuideReadmeDocs({ repositoryRoot: join(__dirname, '..'), websiteRoot: __dirname, }); @@ -541,10 +541,6 @@ const config: UserConfig = defineConfig({ '/api/rspeedy': SIDEBARS.Config, '/zh/api/rspeedy': SIDEBARS_ZH.Config, - // GenUI - '/genui/': GENUI.en, - '/zh/genui/': GENUI.zh, - // API ...(Object.fromEntries( [ @@ -664,6 +660,7 @@ const config: UserConfig = defineConfig({ }, ], }, + GENUI.en, ], '/zh/guide/': [ { @@ -742,6 +739,7 @@ const config: UserConfig = defineConfig({ }, ], }, + GENUI.zh, ], }, nav: [ @@ -753,10 +751,6 @@ const config: UserConfig = defineConfig({ text: 'REPL', link: '/repl', }, - { - text: 'GenUI', - link: '/genui/a2ui-catalog-extractor', - }, { text: 'API', items: [ diff --git a/website/sidebars/genui.ts b/website/sidebars/genui.ts index d6bb899e9b..2ecde8c44d 100644 --- a/website/sidebars/genui.ts +++ b/website/sidebars/genui.ts @@ -4,20 +4,14 @@ import fs from 'node:fs'; import path from 'node:path'; -import type { - SidebarDivider, - SidebarItem, - SidebarSectionHeader, -} from '@rspress/core'; +import type { SidebarGroup } from '@rspress/core'; -type GenUISidebar = (SidebarItem | SidebarSectionHeader | SidebarDivider)[]; - -export function createGenUIReadmeDocs(options: { +export function createGenUIGuideReadmeDocs(options: { repositoryRoot: string; websiteRoot: string; }): { - en: GenUISidebar; - zh: GenUISidebar; + en: SidebarGroup; + zh: SidebarGroup; } { const packageRoot = path.join( options.repositoryRoot, @@ -26,10 +20,10 @@ export function createGenUIReadmeDocs(options: { syncReadme({ languageSwitch: - 'English | 简体中文', + 'English | 简体中文', outFile: path.join( options.websiteRoot, - 'docs/en/genui/a2ui-catalog-extractor.md', + 'docs/en/guide/genui/a2ui-catalog-extractor.md', ), sourceFile: path.join(packageRoot, 'README.md'), switchPattern: /^English \| \[简体中文\]\(\.\/readme\.zh_cn\.md\)$/m, @@ -37,40 +31,34 @@ export function createGenUIReadmeDocs(options: { syncReadme({ languageSwitch: - 'English | 简体中文', + 'English | 简体中文', outFile: path.join( options.websiteRoot, - 'docs/zh/genui/a2ui-catalog-extractor.md', + 'docs/zh/guide/genui/a2ui-catalog-extractor.md', ), sourceFile: path.join(packageRoot, 'readme.zh_cn.md'), switchPattern: /^\[English\]\(\.\/README\.md\) \| 简体中文$/m, }); return { - en: [ - { - sectionHeaderText: 'GenUI', - }, - { - dividerType: 'solid', - }, - { - text: 'A2UI Catalog Extractor', - link: '/genui/a2ui-catalog-extractor', - }, - ], - zh: [ - { - sectionHeaderText: 'GenUI', - }, - { - dividerType: 'solid', - }, - { - text: 'A2UI Catalog Extractor', - link: '/zh/genui/a2ui-catalog-extractor', - }, - ], + en: { + text: 'GenUI', + items: [ + { + text: 'A2UI Catalog Extractor', + link: '/guide/genui/a2ui-catalog-extractor', + }, + ], + }, + zh: { + text: 'GenUI', + items: [ + { + text: 'A2UI Catalog Extractor', + link: '/zh/guide/genui/a2ui-catalog-extractor', + }, + ], + }, }; }