mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 06:31:49 +00:00
OpenAPI: enable Typescript definition generation for request body
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
enable Typescript definition generation for request body
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user