feat(site): extract api reference from components (#464)

This commit is contained in:
Darius Cepulis
2026-02-05 19:57:54 -06:00
committed by GitHub
parent 48364f17fd
commit 0991a899b2
80 changed files with 3425 additions and 1562 deletions
@@ -0,0 +1,240 @@
import { uniq } from 'es-toolkit/array';
import * as tae from 'typescript-api-extractor';
import type { PropDef } from './types.js';
/**
* Get abbreviated type for display in collapsed rows.
*
* Returns `shortType` when abbreviation adds value, `undefined` otherwise.
*/
export function getShortPropType(name: string, type: string): string | undefined {
// Callbacks → "function"
if (/^(on|get)[A-Z]/.test(name) && type.includes('=>')) {
return 'function';
}
// className/style/render → simplified
if (name === 'className' && type.includes('=>')) {
return 'string | function';
}
if (name === 'style' && type.includes('=>')) {
return 'CSSProperties | function';
}
if (name === 'render' && type.includes('=>')) {
return 'ReactElement | function';
}
// Simple types → no abbreviation needed
if (['boolean', 'string', 'number'].includes(type)) {
return undefined;
}
// Short unions (less than 3 members and under 40 chars) → no abbreviation
if (!type.includes(' | ') || (type.split(' | ').length < 3 && type.length < 40)) {
return undefined;
}
// Function in union → "type | function"
if (type.includes('=>')) {
const parts = type.split(' | ');
const nonFunctionParts = parts.filter((p) => !p.includes('=>'));
if (nonFunctionParts.length > 0) {
return `${nonFunctionParts.join(' | ')} | function`;
}
return 'function';
}
// Complex unions → no abbreviation needed (show full type)
return undefined;
}
/**
* Format a list of properties into API reference format.
*/
export function formatProperties(props: tae.PropertyNode[]): Record<string, PropDef> {
const result: Record<string, PropDef> = {};
for (const prop of props) {
// Skip ref for components
if (prop.name === 'ref') continue;
// Skip props marked with @ignore
if (prop.documentation?.hasTag('ignore')) continue;
const formattedType = formatType(prop.type, prop.optional);
const shortType = getShortPropType(prop.name, formattedType);
const entry: PropDef = { type: formattedType };
if (shortType !== undefined) entry.shortType = shortType;
if (prop.documentation?.defaultValue !== undefined) entry.default = prop.documentation.defaultValue;
if (!prop.optional) entry.required = true;
if (prop.documentation?.description !== undefined) entry.description = prop.documentation.description;
result[prop.name] = entry;
}
return result;
}
/**
* Format a type into a human-readable string.
*/
export function formatType(type: tae.AnyType, removeUndefined: boolean): string {
if (type instanceof tae.ExternalTypeNode) {
if (/^ReactElement(<.*>)?/.test(type.typeName.name || '')) {
return 'ReactElement';
}
if (type.typeName.namespaces?.length === 1 && type.typeName.namespaces[0] === 'React') {
return createNameWithTypeArguments(type.typeName);
}
return getFullyQualifiedName(type.typeName);
}
if (type instanceof tae.IntrinsicNode) {
return type.typeName ? getFullyQualifiedName(type.typeName) : type.intrinsic;
}
if (type instanceof tae.UnionNode) {
if (type.typeName) {
return getFullyQualifiedName(type.typeName);
}
let memberTypes = type.types;
if (removeUndefined) {
memberTypes = memberTypes.filter((t) => !(t instanceof tae.IntrinsicNode && t.intrinsic === 'undefined'));
}
const flattenedMemberTypes = memberTypes.flatMap((t) => {
if (t instanceof tae.UnionNode) {
return t.typeName ? t : t.types;
}
if (t instanceof tae.TypeParameterNode && t.constraint instanceof tae.UnionNode) {
return t.constraint.types;
}
return t;
});
const formattedMemberTypes = uniq(orderMembers(flattenedMemberTypes).map((t) => formatType(t, removeUndefined)));
return formattedMemberTypes.join(' | ');
}
if (type instanceof tae.IntersectionNode) {
if (type.typeName) {
return getFullyQualifiedName(type.typeName);
}
return orderMembers(type.types)
.map((t) => formatType(t, false))
.join(' & ');
}
if (type instanceof tae.ObjectNode) {
if (type.typeName) {
return getFullyQualifiedName(type.typeName);
}
if (type.properties.length === 0) {
return '{}';
}
return `{ ${type.properties.map((m) => `${m.name}${m.optional ? '?' : ''}: ${formatType(m.type, m.optional)}`).join('; ')} }`;
}
if (type instanceof tae.LiteralNode) {
return normalizeQuotes(type.value as string);
}
if (type instanceof tae.ArrayNode) {
const formattedMemberType = formatType(type.elementType, false);
if (formattedMemberType.includes(' ')) {
return `(${formattedMemberType})[]`;
}
return `${formattedMemberType}[]`;
}
if (type instanceof tae.FunctionNode) {
if (type.typeName) {
return getFullyQualifiedName(type.typeName);
}
const functionSignature = type.callSignatures
.map((s) => {
const params = s.parameters.map((p) => `${p.name}: ${formatType(p.type, false)}`).join(', ');
const returnType = formatType(s.returnValueType, false);
return `(${params}) => ${returnType}`;
})
.join(' | ');
return `(${functionSignature})`;
}
if (type instanceof tae.TupleNode) {
if (type.typeName) {
return getFullyQualifiedName(type.typeName);
}
return `[${type.types.map((member: tae.AnyType) => formatType(member, false)).join(', ')}]`;
}
if (type instanceof tae.TypeParameterNode) {
return type.constraint !== undefined ? formatType(type.constraint, removeUndefined) : type.name;
}
return 'unknown';
}
function getFullyQualifiedName(typeName: tae.TypeName): string {
const nameWithTypeArgs = createNameWithTypeArguments(typeName);
if (!typeName.namespaces || typeName.namespaces.length === 0) {
return nameWithTypeArgs;
}
return `${typeName.namespaces.join('.')}.${nameWithTypeArgs}`;
}
function createNameWithTypeArguments(typeName: tae.TypeName): string {
if (
typeName.typeArguments &&
typeName.typeArguments.length > 0 &&
typeName.typeArguments.some((ta) => ta.equalToDefault === false)
) {
return `${typeName.name}<${typeName.typeArguments.map((ta) => formatType(ta.type, false)).join(', ')}>`;
}
return typeName.name;
}
/**
* Order members so null, undefined, and any come last.
*/
function orderMembers(members: readonly tae.AnyType[]): readonly tae.AnyType[] {
let ordered = pushToEnd(members, 'any');
ordered = pushToEnd(ordered, 'null');
ordered = pushToEnd(ordered, 'undefined');
return ordered;
}
function pushToEnd(members: readonly tae.AnyType[], name: string): readonly tae.AnyType[] {
const index = members.findIndex(
(member: tae.AnyType) => member instanceof tae.IntrinsicNode && member.intrinsic === name
);
if (index !== -1) {
const member = members[index];
return [...members.slice(0, index), ...members.slice(index + 1), member!];
}
return members;
}
function normalizeQuotes(str: string): string {
if (str.startsWith('"') && str.endsWith('"')) {
return str
.replaceAll("'", "\\'")
.replaceAll('\\"', '"')
.replace(/^"(.*)"$/, "'$1'");
}
return str;
}