Skip to content

Commit 45c7531

Browse files
committed
TypeScript & UI: Support displaying parameters & return types in Type Table
1 parent 2d7d2f9 commit 45c7531

File tree

15 files changed

+417
-174
lines changed

15 files changed

+417
-174
lines changed

.changeset/cyan-mice-sell.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'fumadocs-typescript': patch
3+
'fumadocs-ui': patch
4+
---
5+
6+
Type Table: Support displaying parameters & return types

apps/docs/content/docs/ui/components/auto-type-table.mdx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ description: Auto-generated type table
1111
*
1212
* See https://fumadocs.vercel.app/docs/ui/components/type-table
1313
*/
14-
name: string;
14+
name: boolean | null;
15+
16+
/**
17+
* @param name - user name.
18+
* @param allowNull - is null value allowed.
19+
* @returns user ID
20+
**/
21+
fn: (name: string, allowNull?: boolean) => string
1522
1623
/**
1724
* We love Shiki.
@@ -21,7 +28,7 @@ description: Auto-generated type table
2128
* \`\`\`
2229
* @default { a: "test" }
2330
*/
24-
options: Partial<{ a: unknown }>;
31+
options?: Partial<{ a: unknown }>;
2532
2633
}`} />
2734

packages/typescript/src/lib/base.ts

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,16 @@ export interface DocEntry {
2828
type: string;
2929
simplifiedType: string;
3030

31-
tags: Record<string, string>;
31+
tags: RawTag[];
3232
required: boolean;
3333
deprecated: boolean;
3434
}
3535

36+
export interface RawTag {
37+
name: string;
38+
text: string;
39+
}
40+
3641
interface EntryContext {
3742
program: Project;
3843
transform?: Transformer;
@@ -201,16 +206,21 @@ function getDocEntry(
201206
.getTypeChecker()
202207
.getTypeOfSymbolAtLocation(prop, context.declaration);
203208
const isOptional = prop.isOptional();
204-
const tags = Object.fromEntries(
205-
prop
206-
.getJsDocTags()
207-
.map((tag) => [tag.getName(), ts.displayPartsToString(tag.getText())]),
209+
const tags = prop.getJsDocTags().map(
210+
(tag) =>
211+
({
212+
name: tag.getName(),
213+
text: ts.displayPartsToString(tag.getText()),
214+
}) satisfies RawTag,
208215
);
209216

210-
let type = getFullType(subType, isOptional);
217+
let type = getFullType(subType);
211218

212-
if ('remarks' in tags) {
213-
type = /^`(?<name>.+)`/.exec(tags.remarks)?.[1] ?? type;
219+
for (const tag of tags) {
220+
if (tag.name !== 'remarks') continue;
221+
222+
// replace type with @remarks
223+
type = /^`(?<name>.+)`/.exec(tag.text)?.[1] ?? type;
214224
}
215225

216226
const entry: DocEntry = {
@@ -222,37 +232,27 @@ function getDocEntry(
222232
),
223233
tags,
224234
type,
225-
simplifiedType: getSimpleForm(subType, program.getTypeChecker()),
235+
simplifiedType: getSimpleForm(
236+
subType,
237+
program.getTypeChecker(),
238+
isOptional,
239+
),
226240
required: !isOptional,
227-
deprecated: prop
228-
.getJsDocTags()
229-
.some((tag) => tag.getName() === 'deprecated'),
241+
deprecated: tags.some((tag) => tag.name === 'deprecated'),
230242
};
231243

232244
transform?.call(context, entry, subType, prop);
233245

234246
return entry;
235247
}
236248

237-
function getFullType(type: Type, isOptional: boolean): string {
238-
if (type.isUnion() && isOptional) {
239-
const t = type.compilerType as ts.UnionType;
240-
const originalTypes = t.types;
241-
242-
t.types = t.types.filter((v) => v.flags !== ts.TypeFlags.Undefined);
243-
const result = getFullType(type, false);
244-
t.types = originalTypes;
245-
246-
return result;
247-
}
248-
249-
let typeName = type
250-
.getNonNullableType()
251-
.getText(
252-
undefined,
253-
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope &
254-
ts.TypeFormatFlags.NoTruncation,
255-
);
249+
function getFullType(type: Type): string {
250+
let typeName = type.getText(
251+
undefined,
252+
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope |
253+
ts.TypeFormatFlags.NoTruncation |
254+
ts.TypeFormatFlags.InTypeAlias,
255+
);
256256

257257
if (type.getAliasSymbol() && type.getAliasTypeArguments().length === 0) {
258258
typeName = type.getAliasSymbol()?.getEscapedName() ?? typeName;
Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
11
import * as ts from 'ts-morph';
22

3-
function simplifyType(type: ts.Type, checker: ts.TypeChecker): string {
4-
// Handle union types
3+
export function getSimpleForm(
4+
type: ts.Type,
5+
checker: ts.TypeChecker,
6+
noUndefined = false,
7+
): string {
8+
if (type.isUndefined() && noUndefined) return '';
9+
10+
const alias = type.getAliasSymbol();
11+
if (alias && type.getAliasTypeArguments().length === 0) {
12+
return alias.getEscapedName();
13+
}
14+
515
if (type.isUnion()) {
616
const types: string[] = [];
717
for (const t of type.getUnionTypes()) {
8-
const str = simplifyType(t, checker);
9-
if (str !== 'never') types.unshift(str);
18+
const str = getSimpleForm(t, checker, noUndefined);
19+
if (str.length > 0 && str !== 'never') types.unshift(str);
1020
}
1121

12-
return types.length > 0 ? types.join(' | ') : 'never';
22+
return types.length > 0
23+
? // boolean | null will become true | false | null, need to ensure it's still returned as boolean
24+
types.join(' | ').replace('true | false', 'boolean')
25+
: 'never';
1326
}
1427

15-
// Handle intersection types
1628
if (type.isIntersection()) {
1729
const types: string[] = [];
1830
for (const t of type.getIntersectionTypes()) {
19-
types.unshift(simplifyType(t, checker));
31+
const str = getSimpleForm(t, checker, noUndefined);
32+
if (str.length > 0 && str !== 'never') types.unshift(str);
2033
}
2134

2235
return types.join(' & ');
@@ -25,17 +38,12 @@ function simplifyType(type: ts.Type, checker: ts.TypeChecker): string {
2538
if (type.isTuple()) {
2639
const elements = type
2740
.getTupleElements()
28-
.map((t) => simplifyType(t, checker))
41+
.map((t) => getSimpleForm(t, checker))
2942
.join(', ');
3043

3144
return `[${elements}]`;
3245
}
3346

34-
const alias = type.getAliasSymbol();
35-
if (alias && type.getAliasTypeArguments().length === 0) {
36-
return alias.getEscapedName();
37-
}
38-
3947
if (type.isArray() || type.isReadonlyArray()) {
4048
return 'array';
4149
}
@@ -48,9 +56,9 @@ function simplifyType(type: ts.Type, checker: ts.TypeChecker): string {
4856
return 'object';
4957
}
5058

51-
return type.getText();
52-
}
53-
54-
export function getSimpleForm(type: ts.Type, checker: ts.TypeChecker): string {
55-
return simplifyType(type, checker);
59+
return type.getText(
60+
undefined,
61+
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope |
62+
ts.TypeFormatFlags.InTypeAlias,
63+
);
5664
}

packages/typescript/src/lib/mdx.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import * as path from 'node:path';
22
import {
33
type DocEntry,
44
type GeneratedDoc,
5-
type Generator,
65
type GenerateOptions,
6+
type Generator,
77
} from './base';
88

99
interface Templates {
@@ -39,8 +39,8 @@ ${doc.description}
3939
4040
${c.description || 'No Description'}
4141
42-
${Object.entries(c.tags)
43-
.map(([tag, value]) => `- ${tag}:\n${replaceJsDocLinks(value)}`)
42+
${c.tags
43+
.map((tag) => `- ${tag.name}:\n${replaceJsDocLinks(tag.text)}`)
4444
.join('\n')}
4545
4646
</div>`,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { RawTag } from '@/lib/base';
2+
3+
export interface ParameterTag {
4+
name: string;
5+
description?: string;
6+
}
7+
8+
export interface TypedTags {
9+
default?: string;
10+
params?: ParameterTag[];
11+
returns?: string;
12+
}
13+
14+
/**
15+
* Parse tags, only returns recognized fields.
16+
*/
17+
export function parseTags(tags: RawTag[]): TypedTags {
18+
const typed: TypedTags = {};
19+
20+
for (const { name: key, text } of tags) {
21+
if (key === 'default' || key === 'defaultValue') {
22+
typed.default = text;
23+
continue;
24+
}
25+
26+
if (key === 'param') {
27+
const [param, description] = text.split('-', 2);
28+
29+
typed.params ??= [];
30+
typed.params.push({
31+
name: param.trim(),
32+
description: description.trim(),
33+
});
34+
continue;
35+
}
36+
37+
if (key === 'returns') {
38+
typed.returns = text;
39+
}
40+
}
41+
42+
return typed;
43+
}

0 commit comments

Comments
 (0)