This is the dev preview website. Check out the document at lynxjs.org

A2UI Catalog Extractor

English | 简体中文

@lynx-js/genui/a2ui-catalog-extractor is the internal TypeDoc-powered extraction engine behind genui a2ui generate catalog. It 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 run the public genui a2ui command to generate the JSON Schema that an A2UI agent can read.

For user-facing scripts, use genui a2ui generate catalog. Treat this package as the implementation layer for extraction behavior and tests.

What It Does

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 extractor generates the components part of an A2UI v0.9 catalog:

{
  "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.
  • It does not call an LLM or choose a model.

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

Package manager

Run the public CLI after installing @lynx-js/genui:

genui a2ui --help

Then add a script to your package:

{
  "scripts": {
    "build:catalog": "genui a2ui generate catalog --catalog-dir src/catalog --out-dir dist"
  }
}

Run it with:

pnpm build:catalog

Quick Start

This example walks through a complete component contract from TypeScript interface to generated catalog JSON.

1. Create a catalog-facing interface

Create src/catalog/QuickStartCard.tsx:

/**
 * Quick start card.
 *
 * @remarks Use this contract as a compact card example.
 * @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<string, string | number | boolean>;
}

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:

genui a2ui generate catalog --catalog-dir src/catalog --out-dir dist

The extractor scans the catalog directory, finds interfaces marked with @a2uiCatalog, and writes one file per component:

dist/
  catalog.json
dist/catalog/
  QuickStartCard/
    catalog.json

3. Read the generated schema

dist/catalog/QuickStartCard/catalog.json will look like this:

{
  "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.\n\nUse this contract as a compact card example."
  }
}

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<string, string | number | boolean> 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:

/**
 * @a2uiCatalog Text
 */
export interface TextProps {
  text: string;
}

Do not put the tag on the component function:

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

Component names

You can write the component name explicitly:

/**
 * @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:

/**
 * @a2uiCatalog
 */
export interface DemoTextProps {}

This becomes DemoText.

Comments become schema metadata

Use normal TypeDoc comments:

/**
 * 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 commentJSON Schema output
Summary textdescription
@remarksAppended to description
@defaultValue or @defaultdefault
@deprecateddeprecated: true
Optional property ?Property omitted from required

For object and array defaults, put JSON inside a code span:

/**
 * @defaultValue `{}`
 */
context?: Record<string, string>;

Without the code span, TypeDoc may pass formatted text instead of the raw JSON value.

Supported TypeScript shapes

TypeScript shapeJSON 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<T>{ "type": "array", "items": ... }
ReadonlyArray<T>{ "type": "array", "items": ... }
{ name: string }Strict object with additionalProperties: false
Record<string, T>Object map with additionalProperties: ...

Unsupported or ambiguous types

These types intentionally fail:

  • any
  • unknown
  • null
  • undefined
  • never
  • void
  • nullable unions such as string | null
  • most imported aliases and referenced external interfaces
  • Record<number, T> or other non-string record keys

Prefer explicit catalog contracts:

// Avoid this in catalog-facing interfaces.
type ExternalCardData = {
  title: string;
};

export interface CardProps {
  data: ExternalCardData;
}

Write the shape inline instead:

export interface CardProps {
  data: {
    title: string;
  };
}

CLI Reference

The public CLI entry point is genui a2ui generate catalog. It delegates to this package internally.

Generate catalog artifacts

genui a2ui generate catalog [options]
OptionDescriptionDefault
--catalog-dir <dir>Directory to scan for source files. Repeatable.src/catalog
--source <path>Source file or directory to scan. Repeatable.None
--typedoc-json <file>Read an existing TypeDoc JSON project instead of running TypeDoc conversion.None
--out-dir <dir>Root directory where catalog artifacts are written.dist
--catalog-id <id>Catalog ID written to the full catalog file.catalog.json
--version, -vPrint the package version.None
--help, -hPrint usage.None

--source and --catalog-dir can be used together. The extractor merges all inputs, removes duplicates, sorts them, and then runs TypeDoc.

The extractor writes both per-component files such as dist/catalog/QuickStartCard/catalog.json and a full catalog file at dist/catalog.json.

The scanner accepts .ts, .tsx, .js, .jsx, .mts, and .cts files. It ignores .d.ts, node_modules, dist, and .turbo.

Repository-internal Programmatic API

The package entry point intentionally exposes only the extraction helpers needed by repository tooling and tests. It is not the external integration surface for product code. Product build scripts should call genui a2ui generate catalog instead of importing this package directly.

import {
  createA2UICatalog,
  extractCatalogComponents,
  extractCatalogFunctions,
  findCatalogSourceFiles,
} from '@lynx-js/genui/a2ui-catalog-extractor';

const sourceFiles = findCatalogSourceFiles('src/catalog');
const components = await extractCatalogComponents({
  sourceFiles,
  tsconfig: 'tsconfig.json',
});
const functions = await extractCatalogFunctions({
  sourceFiles,
  tsconfig: 'tsconfig.json',
});

const catalog = createA2UICatalog({
  catalogId: 'https://example.com/catalogs/basic/v1/catalog.json',
  components,
  functions,
  theme: {
    accentColor: { type: 'string' },
  },
});

Use cwd in extraction options when paths should be resolved relative to a specific project directory. Artifact writing and TypeDoc JSON reuse are CLI implementation details; use genui a2ui generate catalog for those flows.

Troubleshooting

Unsupported ambiguous intrinsic TypeDoc type "unknown"

The catalog needs a concrete schema. Replace unknown or any with a specific type:

// Fails.
payload: unknown;

// Works.
payload: {
  id: string;
  count: number;
}

Unsupported nullable union

Nullable unions are not accepted:

// Fails.
label: string | null;

Make the property optional if it can be omitted:

label?: string;

Or model the state explicitly:

label: string | { path: string };

Unsupported TypeDoc reference

The extractor only understands a small set of references: Array<T>, ReadonlyArray<T>, and Record<string, T>. 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.

References