OpenAPI: enable Typescript definition generation for request body

This commit is contained in:
Fuma Nama
2026-02-27 22:23:23 +08:00
parent b59c896d6f
commit 825f401025
7 changed files with 74 additions and 19 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
enable Typescript definition generation for request body
+5 -2
View File
@@ -32,8 +32,12 @@ export type MethodInformation = NoReference<OperationObject> & {
'x-exclusiveCodeSample'?: string;
};
type RequireKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
export interface RenderContext
extends Pick<OpenAPIOptions, 'proxyUrl'>, Omit<CreateAPIPageOptions, 'renderMarkdown'> {
extends
Pick<OpenAPIOptions, 'proxyUrl'>,
RequireKeys<CreateAPIPageOptions, 'generateTypeScriptDefinitions' | 'renderMarkdown'> {
slugger: Slugger;
/**
@@ -48,7 +52,6 @@ export interface RenderContext
text: string,
props?: HTMLAttributes<HTMLHeadingElement>,
) => ReactNode;
renderMarkdown: (text: string) => ReactNode;
renderCodeBlock: (lang: string, code: string) => ReactNode;
}
+29 -1
View File
@@ -23,6 +23,7 @@ import type { ExampleRequestItem } from './operation/request-tabs';
import type { ResolvedShikiConfig } from 'fumadocs-core/highlight/config';
import { APIPage, type ApiPageProps, type OperationItem, type WebhookItem } from './api-page';
import type { CodeUsageGeneratorRegistry, InlineCodeUsageGenerator } from '@/requests/generators';
import type { JSONSchema } from 'json-schema-typed';
export interface CreateAPIPageOptions {
/**
@@ -32,6 +33,7 @@ export interface CreateAPIPageOptions {
*
* @param method - the operation object
* @param statusCode - status code
* @deprecated use `generateTypeScriptDefinitions` instead.
*/
generateTypeScriptSchema?:
| ((
@@ -42,6 +44,23 @@ export interface CreateAPIPageOptions {
) => Awaitable<string | undefined>)
| false;
/**
* Generate TypeScript definitions from JSON schema.
*
* Pass `false` to disable it.
*/
generateTypeScriptDefinitions?: (
schema: JSONSchema,
ctx: RenderContext & {
operation: NoReference<MethodInformation>;
/** @deprecated */
_internal_legacy?: {
statusCode: string;
contentType: string;
};
},
) => Awaitable<string | undefined>;
/**
* Generate example code usage for endpoints.
*/
@@ -53,7 +72,7 @@ export interface CreateAPIPageOptions {
generateCodeSamples?: (method: MethodInformation) => Awaitable<InlineCodeUsageGenerator[]>;
shiki: ResolvedShikiConfig;
renderMarkdown?: (md: string) => Awaitable<ReactNode>;
renderMarkdown?: (md: string) => ReactNode;
shikiOptions?: DistributiveOmit<CoreHighlightOptions, 'config' | 'lang' | 'components'>;
/**
@@ -236,6 +255,15 @@ export function createAPIPage(
</Heading>
);
},
generateTypeScriptDefinitions(schema, ctx) {
const { generateTypeScriptSchema, generateTypeScriptDefinitions } = options;
if (generateTypeScriptSchema && ctx._internal_legacy) {
const { statusCode, contentType } = ctx._internal_legacy;
return generateTypeScriptSchema(ctx.operation, statusCode, contentType, ctx);
}
return generateTypeScriptDefinitions?.(schema, ctx);
},
async renderMarkdown(text) {
if (options.renderMarkdown) return options.renderMarkdown(text);
processor ??= createMarkdownProcessor();
+2 -3
View File
@@ -11,9 +11,8 @@ export function createAPIPage(server: OpenAPIServer, options: CreateAPIPageOptio
const APIPage = base.createAPIPage(server, {
...options,
shiki: configDefault,
generateTypeScriptSchema(method, statusCode, contentType, ctx) {
const schema = method.responses?.[statusCode]?.content?.[contentType].schema;
if (!schema || typeof schema !== 'object') return;
generateTypeScriptDefinitions(schema, ctx) {
if (typeof schema !== 'object') return;
return getTypescriptSchema(schema, ctx);
},
});
+8 -4
View File
@@ -13,7 +13,13 @@ import {
SelectValue,
} from '@/ui/components/select';
export function CopyResponseTypeScript({ code }: { code: string }) {
export function CopyTypeScriptPanel({
name,
code,
}: {
code: string;
name: 'response body' | 'request body';
}) {
const [isChecked, onCopy] = useCopyButton(() => {
void navigator.clipboard.writeText(code);
});
@@ -22,9 +28,7 @@ export function CopyResponseTypeScript({ code }: { code: string }) {
<div className="flex items-start justify-between gap-2 bg-fd-card text-fd-card-foreground border rounded-xl p-3 not-prose mb-4 last:mb-0">
<div>
<p className="font-medium text-sm mb-2">TypeScript Definitions</p>
<p className="text-xs text-fd-muted-foreground">
Use the response body type in TypeScript.
</p>
<p className="text-xs text-fd-muted-foreground">Use the {name} type in TypeScript.</p>
</div>
<button
onClick={onCopy}
+19 -8
View File
@@ -10,7 +10,7 @@ import { idToTitle } from '@/utils/id-to-title';
import { Schema } from '../schema';
import { UsageTabs } from '@/ui/operation/usage-tabs';
import { Badge, MethodLabel } from '@/ui/components/method-label';
import { CopyResponseTypeScript, SelectTab, SelectTabs, SelectTabTrigger } from './client';
import { CopyTypeScriptPanel, SelectTab, SelectTabs, SelectTabTrigger } from './client';
import {
AccordionContent,
AccordionHeader,
@@ -90,13 +90,21 @@ export async function Operation({
)}
</div>
{body.description && ctx.renderMarkdown(body.description)}
{contentTypes.map(([type, content]) => {
{contentTypes.map(async ([type, content]) => {
if (!isMediaTypeSupported(type, ctx.mediaAdapters)) {
throw new Error(`Media type ${type} is not supported (in ${path})`);
}
const ts = content.schema
? await ctx.generateTypeScriptDefinitions(content.schema, {
operation: method,
...ctx,
})
: undefined;
return (
<SelectTab key={type} value={type}>
{ts && <CopyTypeScriptPanel name="request body" code={ts} />}
<Schema
client={{
name: 'body',
@@ -380,15 +388,18 @@ async function ResponseAccordion({
)}
{contentTypes.map(async ([type, resType]) => {
const schema = resType.schema;
let ts: string | undefined;
if (ctx.generateTypeScriptSchema) {
ts = await ctx.generateTypeScriptSchema(operation, status, type, ctx);
}
const ts = await ctx.generateTypeScriptDefinitions(operation, {
operation,
_internal_legacy: {
statusCode: status,
contentType: type,
},
...ctx,
});
return (
<SelectTab key={type} value={type} className="mb-2">
{ts && <CopyResponseTypeScript code={ts} />}
{ts && <CopyTypeScriptPanel name="response body" code={ts} />}
{schema && (
<div className="border px-3 py-2 rounded-lg">
<Schema
+6 -1
View File
@@ -254,7 +254,12 @@ function ObjectSearchContent({
firstItemRef.current = filtered.length > 0 ? filtered[0] : null;
if (filtered.length === 0)
return <p className="text-fd-muted-foreground text-sm">No props matching {`"${rawSearch}"`}</p>;
return (
<p className="text-fd-muted-foreground text-sm px-2">
No property matching{' '}
<span className="text-fd-foreground font-medium">{`"${rawSearch}"`}</span>
</p>
);
return filtered.map((prop) => (
<SchemaUIProperty