feat(openapi): support components.Heading in client config

This commit is contained in:
Fuma Nama
2026-06-03 19:12:35 +08:00
parent 2d0eb9a52d
commit 8eb7782915
15 changed files with 60 additions and 42 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Support `components.Heading` in client config
-1
View File
@@ -52,7 +52,6 @@
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"chokidar": "^5.0.0",
"class-variance-authority": "^0.7.1",
"github-slugger": "^2.0.0",
+12 -4
View File
@@ -1,6 +1,14 @@
export * from './generate-file';
export type { MediaAdapter } from '@/requests/media/adapter';
export type * from './types';
export type * from './utils/pages/builder';
export type * from './utils/pages/preset-auto';
export type { MethodInformation, RenderContext } from './types';
export type {
OperationOutput,
OutputEntry,
OutputGroup,
PageOutput,
PagesBuilder,
PagesBuilderConfig,
WebhookOutput,
} from './utils/pages/builder';
export type { SchemaToPagesOptions } from './utils/pages/preset-auto';
export type { OpenAPIV3_2, OpenAPIV3_1, OpenAPIV3, OpenAPIV2, OpenAPI } from '@/_openapi/types';
+1 -1
View File
@@ -107,7 +107,7 @@ export interface PlaygroundClientOptions {
* for updating values, use:
* - the `Custom.useController()` from `fumadocs-openapi/playground/client`.
*
* Recommended types packages: `json-schema-typed`, `openapi-types`.
* Recommended types packages: `json-schema-typed`.
*/
renderParameterField?: (fieldName: FieldKey, param: ParameterObject) => ReactNode;
+5 -5
View File
@@ -4,7 +4,6 @@ import type { DereferencedDocument } from '@/utils/document/dereference';
import type { MediaAdapter } from '@/requests/media/adapter';
import type { OpenAPIOptions } from '@/server';
import type { CreateAPIPageOptions } from './ui/base';
import type { HTMLAttributes, ReactNode } from 'react';
import type { InlineCodeUsageGenerator } from './requests/generators';
export type Document = OpenAPIV3_2.Document;
@@ -37,8 +36,11 @@ export interface RenderContext
extends
Pick<OpenAPIOptions, 'proxyUrl'>,
Omit<
RequireKeys<CreateAPIPageOptions, 'renderMarkdown' | 'generateTypeScriptDefinitions'>,
'renderCodeBlock' | 'renderHeading' | 'generateTypeScriptSchema'
RequireKeys<
CreateAPIPageOptions,
'renderMarkdown' | 'generateTypeScriptDefinitions' | 'renderCodeBlock'
>,
'renderHeading' | 'generateTypeScriptSchema'
> {
/**
* dereferenced schema
@@ -47,8 +49,6 @@ export interface RenderContext
clientBoundary: typeof import('@/ui/client/boundary');
mediaAdapters: Record<string, MediaAdapter>;
renderCodeBlock: (lang: string, code: string) => ReactNode;
}
export type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
+2 -5
View File
@@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any -- rehype-react without types */
import Slugger from 'github-slugger';
import type { Awaitable, MethodInformation, RenderContext } from '@/types';
import { parseSecurities, type NoReference } from '@/utils/schema';
import type { DereferencedDocument } from '@/utils/document/dereference';
@@ -7,7 +6,6 @@ import { defaultAdapters, MediaAdapter } from '@/requests/media/adapter';
import type { FC, HTMLAttributes, ReactNode } from 'react';
import type { OpenAPIServer } from '@/server';
import type { APIPageClientOptions } from './client';
import { Heading } from 'fumadocs-ui/components/heading';
import { createRehypeCode } from 'fumadocs-core/mdx-plugins/rehype-code.core';
import { remarkGfm } from 'fumadocs-core/mdx-plugins/remark-gfm';
import defaultMdxComponents from 'fumadocs-ui/mdx';
@@ -203,7 +201,7 @@ export interface CreateAPIPageOptions {
*/
provider?: (props: { children: ReactNode }) => ReactNode;
/**
* replace the server-side renderer
* replace the renderer
*/
render?: (props: APIPlaygroundProps) => ReactNode;
};
@@ -276,7 +274,6 @@ export function createAPIPage(
processed = document;
}
const slugger = new Slugger();
const { ApiProvider, PlaygroundClient, SchemaUI, ServerProvider, UsageTab, UsageTabsSelector } =
await import('@/ui/client/boundary.lazy');
@@ -331,7 +328,7 @@ export function createAPIPage(
return out.result as ReactNode;
},
async renderCodeBlock(lang, code) {
async renderCodeBlock({ lang, code }) {
if (options.renderCodeBlock) {
return options.renderCodeBlock({ lang, code });
}
+13 -11
View File
@@ -1,13 +1,24 @@
'use client';
import type { PlaygroundClientOptions } from '@/playground/client';
import type { MediaAdapter } from '@/requests/media/adapter';
import type { FC } from 'react';
import type { ComponentProps, FC } from 'react';
import type { CodeUsageGeneratorRegistry } from '@/requests/generators';
import type { ExampleRequestItem } from '../operation/get-example-requests';
export interface APIPageClientOptions {
playground?: PlaygroundClientOptions;
operation?: OperationClientOptions;
operation?: {
APIExampleSelector?: FC<{
items: ExampleRequestItem[];
value: string | undefined;
onValueChange: (id: string) => void;
}>;
};
components?: {
Heading?: FC<ComponentProps<'h1'> & { id: string; depth: number }>;
};
/**
* Set a prefix for `localStorage` keys.
@@ -29,15 +40,6 @@ export interface APIPageClientOptions {
codeUsages?: CodeUsageGeneratorRegistry;
}
export interface OperationClientOptions {
APIExampleSelector?: FC<{
items: ExampleRequestItem[];
value: string | undefined;
onValueChange: (id: string) => void;
}>;
}
export function defineClientConfig(options: APIPageClientOptions = {}): APIPageClientOptions {
return options;
}
@@ -2,6 +2,7 @@
import { useAnchorId } from '@/utils/auto-anchor.client';
import type { ComponentProps } from 'react';
import { Heading as BaseHeading } from 'fumadocs-ui/components/heading';
import { useApiContext } from '../contexts/api';
export function Heading({
id: _id,
@@ -9,6 +10,8 @@ export function Heading({
...props
}: ComponentProps<'h1'> & { id: string; depth: number }) {
const id = useAnchorId([_id]);
const Component = useApiContext().client.components?.Heading;
if (Component) return <Component id={id} depth={depth} {...props} />;
return <BaseHeading id={id} as={`h${depth}` as `h1`} {...props} />;
}
+9 -8
View File
@@ -1,3 +1,4 @@
'use client';
/* eslint-disable @typescript-eslint/no-explicit-any -- rehype-react without types */
import type { Document, RenderContext } from '@/types';
import { defaultAdapters } from '@/requests/media/adapter';
@@ -9,7 +10,6 @@ import {
type FC,
type ReactNode,
} from 'react';
import { Heading } from 'fumadocs-ui/components/heading';
import { remarkGfm } from 'fumadocs-core/mdx-plugins/remark-gfm';
import defaultMdxComponents from 'fumadocs-ui/mdx';
import { remark } from 'remark';
@@ -21,11 +21,11 @@ import type { APIPlaygroundProps, CreateAPIPageOptions } from './base';
import { defaultShikiFactory } from 'fumadocs-core/highlight/shiki/full';
import { compile } from '@fumari/json-schema-ts';
import { ClientCodeBlock, ClientCodeBlockProvider } from './components/codeblock';
import { slug } from 'github-slugger';
import * as ClientBoundary from '@/ui/client/boundary';
import { dereferenceDocument } from '@/utils/document/dereference';
import { parseSecurities } from '@/utils/schema';
import { AuthProvider } from '@/playground/auth';
import { APIPageClientOptions } from './client';
export interface ClientApiPageProps extends Omit<ApiPageProps, 'document'> {
payload: ClientApiPagePayload;
@@ -36,9 +36,9 @@ export interface ClientApiPagePayload {
proxyUrl?: string;
}
export type CreateClientAPIPageOptions = Omit<
Partial<CreateAPIPageOptions>,
'generateTypeScriptSchema'
export type CreateClientAPIPageOptions = Partial<
Omit<CreateAPIPageOptions, 'generateTypeScriptSchema' | 'client'> &
Omit<APIPageClientOptions, 'mediaAdapters' | 'codeUsages'>
>;
/**
@@ -115,6 +115,7 @@ export function createClientAPIPage({
shikiOptions,
generateTypeScriptDefinitions,
clientBoundary: ClientBoundary,
client: options,
...options,
mediaAdapters: {
...defaultAdapters,
@@ -133,12 +134,12 @@ export function createClientAPIPage({
value: text,
}).result as ReactNode;
},
renderCodeBlock(lang, code) {
renderCodeBlock(props) {
if (options.renderCodeBlock) {
return options.renderCodeBlock({ lang, code });
return options.renderCodeBlock(props);
}
return <ClientCodeBlock lang={lang} code={code} />;
return <ClientCodeBlock {...props} />;
},
}),
[payload.proxyUrl, processed],
@@ -122,7 +122,10 @@ function RequestTabsItem({
<AccordionTrigger>{v}</AccordionTrigger>
</AccordionHeader>
<AccordionContent className="prose-no-margin">
{ctx.renderCodeBlock('json', JSON.stringify(data, null, 2))}
{ctx.renderCodeBlock({
lang: 'json',
code: JSON.stringify(data, null, 2),
})}
</AccordionContent>
</AccordionItem>
);
@@ -98,7 +98,10 @@ function renderResponseTabsDefault(tabs: ResponseTab[], ctx: RenderContext): Rea
return (
<>
{example.description && ctx.renderMarkdown(example.description)}
{ctx.renderCodeBlock('json', JSON.stringify(example.sample, null, 2))}
{ctx.renderCodeBlock({
lang: 'json',
code: JSON.stringify(example.sample, null, 2),
})}
</>
);
}
+1 -1
View File
@@ -13,7 +13,7 @@ export async function renderContextFrom(input: string): Promise<RenderContext> {
},
shiki: defaultShikiFactory,
shikiOptions: { theme: 'github-light' },
renderCodeBlock(_lang, code) {
renderCodeBlock({ code }) {
return code;
},
clientBoundary: ClientBoundary,
+1 -1
View File
@@ -5,7 +5,7 @@
"compilerOptions": {
"noEmit": true,
"jsx": "react-jsx",
"lib": ["ESNext", "DOM"],
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
"target": "ES2020"
}
-3
View File
@@ -2757,9 +2757,6 @@ importers:
'@radix-ui/react-select':
specifier: ^2.2.6
version: 2.2.6(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@radix-ui/react-slot':
specifier: ^1.2.4
version: 1.2.4(@types/[email protected])([email protected])
chokidar:
specifier: ^5.0.0
version: 5.0.0