Skip to content
Open
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
6 changes: 4 additions & 2 deletions packages/scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"private": true,
"type": "module",
"scripts": {
"generate-skills": "tsx src/generate-skills.ts"
"generate-skills": "tsx src/generate-skills.ts",
"test": "vitest run"
},
"dependencies": {
"gray-matter": "^4.0.3"
Expand All @@ -13,6 +14,7 @@
"@repo/typescript-config": "workspace:*",
"@types/node": "^25.3.5",
"tsx": "^4.19.0",
"typescript": "5.9.3"
"typescript": "5.9.3",
"vitest": "^4.0.17"
}
}
78 changes: 2 additions & 76 deletions packages/scripts/src/generate-skills.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import matter from "gray-matter";
// Node.js script - Node.js modules are valid here
// oxlint-disable-next-line eslint-plugin-import(no-nodejs-modules)
import { existsSync, mkdirSync, rmSync } from "node:fs";
Expand All @@ -6,7 +7,7 @@ import { readdir, readFile, writeFile } from "node:fs/promises";
// oxlint-disable-next-line eslint-plugin-import(no-nodejs-modules)
import { basename, join } from "node:path";

import matter from "gray-matter";
import { replaceTypeTables } from "./type-table.js";

const ROOT_DIR = join(import.meta.dirname, "../../..");
const CONTENT_DIR = join(ROOT_DIR, "apps/docs/content");
Expand Down Expand Up @@ -44,81 +45,6 @@ const replaceInstaller = (content: string): string =>
`\`\`\`bash\nnpx ai-elements@latest add ${component}\n\`\`\``
);

const PROP_REGEX = /['"]?([^'":\s]+)['"]?\s*:\s*\{([^}]+)\}/g;
const DESC_REGEX = /description:\s*['"]([^'"]+)['"]/;
const TYPE_REGEX = /type:\s*['"]([^'"]+)['"]/;
const DEFAULT_REGEX = /default:\s*['"]([^'"]+)['"]/;
const REQUIRED_REGEX = /required:\s*true/;

const parseTypeTableProps = (
typeContent: string
): {
name: string;
type: string;
description: string;
required?: boolean;
default?: string;
}[] => {
const props: {
name: string;
type: string;
description: string;
required?: boolean;
default?: string;
}[] = [];

const matches = typeContent.matchAll(PROP_REGEX);

for (const match of matches) {
const [, propName, propBody] = match;

const descMatch = propBody.match(DESC_REGEX);
const typeMatch = propBody.match(TYPE_REGEX);
const defaultMatch = propBody.match(DEFAULT_REGEX);
const requiredMatch = propBody.match(REQUIRED_REGEX);

props.push({
default: defaultMatch?.[1],
description: descMatch?.[1] || "",
name: propName,
required: !!requiredMatch,
type: typeMatch?.[1] || "unknown",
});
}

return props;
};

const replaceTypeTables = (content: string): string => {
const typeTableRegex = /<TypeTable\s+type=\{\{([\s\S]*?)\}\}\s*\/>/g;

return content.replace(typeTableRegex, (_, typeContent) => {
const props = parseTypeTableProps(typeContent);

if (props.length === 0) {
return "";
}

const rows = props.map((prop) => {
const name = `\`${prop.name}\``;
const type = `\`${prop.type}\``;
let defaultVal = "-";
if (prop.required) {
defaultVal = "Required";
} else if (prop.default) {
defaultVal = `\`${prop.default}\``;
}
return `| ${name} | ${type} | ${defaultVal} | ${prop.description} |`;
});

return [
"| Prop | Type | Default | Description |",
"|------|------|---------|-------------|",
...rows,
].join("\n");
});
};

const removeCallouts = (content: string): string =>
content.replaceAll(/<Callout[^>]*>[\s\S]*?<\/Callout>/g, "");

Expand Down
80 changes: 80 additions & 0 deletions packages/scripts/src/type-table.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, expect, it } from "vitest";

import { parseTypeTableProps, replaceTypeTables } from "./type-table.js";

describe("type table prop parsing", () => {
it("preserves quoted unions, indexed access types, and object defaults", () => {
const props = parseTypeTableProps(`
selectionMode: {
description: "Whether one or many choices can be selected.",
type: '"single" | "multiple"',
default: '"single"',
},
onLoad: {
description: "Called when loading starts.",
type: 'RiveParameters["onLoad"]',
},
defaultValue: {
description: "The initial draft.",
type: "QuestionValue",
default: '{ selectedValues: [], text: "" }',
},
`);

expect(props).toStrictEqual([
{
default: '"single"',
description: "Whether one or many choices can be selected.",
name: "selectionMode",
type: '"single" | "multiple"',
},
{
description: "Called when loading starts.",
name: "onLoad",
type: 'RiveParameters["onLoad"]',
},
{
default: '{ selectedValues: [], text: "" }',
description: "The initial draft.",
name: "defaultValue",
type: "QuestionValue",
},
]);
});

it("preserves required props and quoted property names", () => {
const props = parseTypeTableProps(`
"...props": {
description: "Props forwarded to the input.",
type: 'Omit<React.ComponentProps<"input">, "value">',
required: true,
},
`);

expect(props).toStrictEqual([
{
description: "Props forwarded to the input.",
name: "...props",
required: true,
type: 'Omit<React.ComponentProps<"input">, "value">',
},
]);
});
});

describe("type table Markdown replacement", () => {
it("escapes union separators in Markdown tables", () => {
const content = `<TypeTable
type={{
mode: {
description: "Choose one | or many.",
type: '"single" | "multiple"',
},
}}
/>`;

expect(replaceTypeTables(content)).toContain(
'| `mode` | `"single" \\| "multiple"` | - | Choose one \\| or many. |'
);
});
});
120 changes: 120 additions & 0 deletions packages/scripts/src/type-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import ts from "typescript";

export interface TypeTableProp {
name: string;
type: string;
description: string;
required?: boolean;
default?: string;
}

const getPropertyName = (name: ts.PropertyName): string | undefined => {
if (
ts.isIdentifier(name) ||
ts.isStringLiteral(name) ||
ts.isNumericLiteral(name)
) {
return name.text;
}

return undefined;
};

const getStringValue = (expression: ts.Expression): string | undefined => {
if (
ts.isStringLiteral(expression) ||
ts.isNoSubstitutionTemplateLiteral(expression)
) {
return expression.text;
}

return undefined;
};

export const parseTypeTableProps = (typeContent: string): TypeTableProp[] => {
const sourceFile = ts.createSourceFile(
"type-table.ts",
`const typeTable = {${typeContent}};`,
ts.ScriptTarget.Latest,
false,
ts.ScriptKind.TS
);
const statement = sourceFile.statements.find(ts.isVariableStatement);
const initializer = statement?.declarationList.declarations[0]?.initializer;

if (!initializer || !ts.isObjectLiteralExpression(initializer)) {
return [];
}

return initializer.properties.flatMap((property) => {
if (
!ts.isPropertyAssignment(property) ||
!ts.isObjectLiteralExpression(property.initializer)
) {
return [];
}

const name = getPropertyName(property.name);
if (!name) {
return [];
}

const prop: TypeTableProp = {
description: "",
name,
type: "unknown",
};

for (const field of property.initializer.properties) {
if (!ts.isPropertyAssignment(field)) {
continue;
}

const fieldName = getPropertyName(field.name);
if (fieldName === "description") {
prop.description = getStringValue(field.initializer) ?? "";
} else if (fieldName === "type") {
prop.type = getStringValue(field.initializer) ?? "unknown";
} else if (fieldName === "default") {
prop.default = getStringValue(field.initializer);
} else if (fieldName === "required") {
prop.required = field.initializer.kind === ts.SyntaxKind.TrueKeyword;
}
}

return [prop];
});
};

const escapeTableCell = (value: string): string =>
value.replaceAll("|", "\\|").replaceAll(/\r?\n/g, " ");

export const replaceTypeTables = (content: string): string => {
const typeTableRegex = /<TypeTable\s+type=\{\{([\s\S]*?)\}\}\s*\/>/g;

return content.replace(typeTableRegex, (_, typeContent: string) => {
const props = parseTypeTableProps(typeContent);

if (props.length === 0) {
return "";
}

const rows = props.map((prop) => {
const name = `\`${escapeTableCell(prop.name)}\``;
const type = `\`${escapeTableCell(prop.type)}\``;
let defaultValue = "-";
if (prop.required) {
defaultValue = "Required";
} else if (prop.default) {
defaultValue = `\`${escapeTableCell(prop.default)}\``;
}
return `| ${name} | ${type} | ${defaultValue} | ${escapeTableCell(prop.description)} |`;
});

return [
"| Prop | Type | Default | Description |",
"|------|------|---------|-------------|",
...rows,
].join("\n");
});
};
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions skills/ai-elements/references/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,28 +89,28 @@ export default function Page() {

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `...props` | `React.ComponentProps<` | - | Any props are spread to the root div. |
| `...props` | `React.ComponentProps<"div">` | - | Any props are spread to the root div. |

### `<AgentHeader />`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `string` | Required | The name of the agent. |
| `model` | `string` | - | The model identifier (e.g. |
| `...props` | `React.ComponentProps<` | - | Any other props are spread to the container div. |
| `model` | `string` | - | The model identifier (e.g. "anthropic/claude-sonnet-4-5"). |
| `...props` | `React.ComponentProps<"div">` | - | Any other props are spread to the container div. |

### `<AgentContent />`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `...props` | `React.ComponentProps<` | - | Any other props are spread to the container div. |
| `...props` | `React.ComponentProps<"div">` | - | Any other props are spread to the container div. |

### `<AgentInstructions />`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `string` | Required | The instruction text. |
| `...props` | `React.ComponentProps<` | - | Any other props are spread to the container div. |
| `...props` | `React.ComponentProps<"div">` | - | Any other props are spread to the container div. |

### `<AgentTools />`

Expand All @@ -131,4 +131,4 @@ export default function Page() {
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `schema` | `string` | Required | The output schema as a string (displayed with syntax highlighting). |
| `...props` | `React.ComponentProps<` | - | Any other props are spread to the container div. |
| `...props` | `React.ComponentProps<"div">` | - | Any other props are spread to the container div. |
Loading