mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
352 lines
11 KiB
TypeScript
352 lines
11 KiB
TypeScript
import { uniq } from 'es-toolkit/array';
|
|
import * as tae from 'typescript-api-extractor';
|
|
import type { PropDef } from './types.js';
|
|
|
|
/**
|
|
* Detect if a type string is a single function type (vs a top-level union).
|
|
*
|
|
* Tracks bracket depth to find the matching `)` for the opening `(` of the parameter list,
|
|
* then checks if `=>` follows. Returns `false` for top-level unions that happen to contain
|
|
* a function member (e.g., `((state: object) => string) | undefined`).
|
|
*/
|
|
function isFunctionType(type: string): boolean {
|
|
if (!type.startsWith('(')) return false;
|
|
let depth = 0;
|
|
for (let i = 0; i < type.length; i++) {
|
|
if (type[i] === '(' || type[i] === '{' || type[i] === '[') depth++;
|
|
else if (type[i] === ')' || type[i] === '}' || type[i] === ']') depth--;
|
|
if (depth === 0) {
|
|
return type
|
|
.slice(i + 1)
|
|
.trimStart()
|
|
.startsWith('=>');
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get abbreviated type for display in collapsed rows.
|
|
*
|
|
* Returns an abbreviated string when abbreviation adds value, `undefined` otherwise.
|
|
*/
|
|
export function abbreviateType(name: string, type: string): string | undefined {
|
|
// Pure function types (no union) → "function"
|
|
// Also matches function types whose return is a union (e.g., `(state: object) => X | undefined`)
|
|
if (type.includes('=>') && (!type.includes(' | ') || isFunctionType(type))) {
|
|
return 'function';
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Object literal > 40 chars → "object"
|
|
if (type.startsWith('{ ') && type.length > 40) {
|
|
return 'object';
|
|
}
|
|
|
|
// Short unions (less than 3 members and under 40 chars) → no abbreviation
|
|
if (!type.includes(' | ') || (type.split(' | ').length < 3 && type.length < 40 && !type.includes('=>'))) {
|
|
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';
|
|
}
|
|
|
|
// Any other type > 40 chars → truncated for display, full in detailedType
|
|
if (type.length > 40) {
|
|
return `${type.slice(0, 37)}...`;
|
|
}
|
|
|
|
// 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[], allExports?: tae.ExportNode[]): 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 expandedType = allExports
|
|
? formatDetailedType(prop.type, allExports, prop.optional)
|
|
: formatType(prop.type, prop.optional);
|
|
const abbreviated = abbreviateType(prop.name, expandedType);
|
|
|
|
const entry: PropDef = { type: abbreviated ?? expandedType };
|
|
if (abbreviated && expandedType !== abbreviated) entry.detailedType = expandedType;
|
|
if (prop.documentation?.defaultValue !== undefined) entry.default = String(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, expanding type aliases when possible.
|
|
*
|
|
* Resolves `ExternalTypeNode` references against `allExports` so that type aliases
|
|
* like `TimeType` are expanded to their underlying union (`'current' | 'duration' | 'remaining'`).
|
|
*/
|
|
export function formatDetailedType(
|
|
type: tae.AnyType,
|
|
allExports: tae.ExportNode[],
|
|
removeUndefined: boolean,
|
|
visited: Set<string> = new Set()
|
|
): string {
|
|
if (type instanceof tae.ExternalTypeNode) {
|
|
const name = type.typeName.name;
|
|
|
|
if (!visited.has(name)) {
|
|
const resolved = allExports.find((exp) => exp.name === name && exp.reexportedFrom === undefined);
|
|
if (resolved) {
|
|
visited.add(name);
|
|
return formatDetailedType(resolved.type, allExports, removeUndefined, visited);
|
|
}
|
|
}
|
|
|
|
return formatType(type, removeUndefined);
|
|
}
|
|
|
|
if (type instanceof tae.UnionNode) {
|
|
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 &&
|
|
t.constraint.types.length <= 5
|
|
) {
|
|
return t.constraint.types;
|
|
}
|
|
return t;
|
|
});
|
|
|
|
const formattedMemberTypes = uniq(
|
|
orderMembers(flattenedMemberTypes).map((t) => formatDetailedType(t, allExports, removeUndefined, visited))
|
|
);
|
|
|
|
return formattedMemberTypes.join(' | ');
|
|
}
|
|
|
|
if (type instanceof tae.IntersectionNode) {
|
|
return orderMembers(type.types)
|
|
.map((t) => formatDetailedType(t, allExports, false, visited))
|
|
.join(' & ');
|
|
}
|
|
|
|
return formatType(type, removeUndefined);
|
|
}
|
|
|
|
/**
|
|
* 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 &&
|
|
t.constraint.types.length <= 5
|
|
) {
|
|
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 'object';
|
|
}
|
|
|
|
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) {
|
|
if (type.constraint === undefined) return type.name;
|
|
// Large union constraints (e.g., keyof JSX.IntrinsicElements) — show the parameter name
|
|
if (type.constraint instanceof tae.UnionNode && type.constraint.types.length > 5) return type.name;
|
|
return formatType(type.constraint, removeUndefined);
|
|
}
|
|
|
|
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;
|
|
}
|