mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: Drop renderer API
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'fumadocs-openapi': major
|
||||
---
|
||||
|
||||
**Drop `renderer` API**
|
||||
|
||||
Fumadocs OpenAPI now expects per-feature customizations, dropping the old centralized `renderer` API.
|
||||
@@ -61,10 +61,7 @@ import {
|
||||
SchemaProvider,
|
||||
useResolvedSchema,
|
||||
} from '@/playground/schema';
|
||||
import {
|
||||
useRequestDataUpdater,
|
||||
useRequestInitialData,
|
||||
} from '@/ui/contexts/code-example';
|
||||
import { useOperationContext } from '@/ui/contexts/operation';
|
||||
import { useEffectEvent } from 'fumadocs-core/utils/use-effect-event';
|
||||
import {
|
||||
Select,
|
||||
@@ -169,8 +166,11 @@ export default function Client({
|
||||
...rest
|
||||
}: ClientProps) {
|
||||
const { server } = useServerSelectContext();
|
||||
const { key: requestDataKey, data: requestData } = useRequestInitialData();
|
||||
const updater = useRequestDataUpdater();
|
||||
const {
|
||||
example: exampleId,
|
||||
examples,
|
||||
setExampleData,
|
||||
} = useOperationContext();
|
||||
const fieldInfoMap = useMemo(() => new Map<string, FieldInfo>(), []);
|
||||
const { mediaAdapters } = useApiContext();
|
||||
const [securityId, setSecurityId] = useState(0);
|
||||
@@ -179,16 +179,19 @@ export default function Client({
|
||||
transformAuthInputs,
|
||||
);
|
||||
|
||||
const defaultValues: FormValues = useMemo(
|
||||
() => ({
|
||||
const defaultValues: FormValues = useMemo(() => {
|
||||
const requestData = examples.find(
|
||||
(example) => example.id === exampleId,
|
||||
)!.data;
|
||||
|
||||
return {
|
||||
path: requestData.path,
|
||||
query: requestData.query,
|
||||
header: requestData.header,
|
||||
body: requestData.body,
|
||||
cookie: requestData.cookie,
|
||||
}),
|
||||
[requestData],
|
||||
);
|
||||
};
|
||||
}, [examples, exampleId]);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
defaultValues,
|
||||
@@ -238,7 +241,7 @@ export default function Client({
|
||||
bodyMediaType: body?.mediaType,
|
||||
};
|
||||
values._encoded ??= encodeRequestData(data, mediaAdapters, parameters);
|
||||
updater.setData(data, values._encoded);
|
||||
setExampleData(data, values._encoded);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -264,6 +267,13 @@ export default function Client({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mounted once only
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
form.reset(initAuthValues(defaultValues, inputs));
|
||||
|
||||
return () => fieldInfoMap.clear();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- ignore other parts
|
||||
}, [defaultValues]);
|
||||
|
||||
useEffect(() => {
|
||||
form.reset((values) => initAuthValues(values, inputs));
|
||||
|
||||
@@ -276,17 +286,9 @@ export default function Client({
|
||||
return values;
|
||||
});
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mounted once only
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- ignore other parts
|
||||
}, [inputs]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
fieldInfoMap.clear();
|
||||
form.reset(initAuthValues(defaultValues, inputs));
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- for on change
|
||||
}, [requestDataKey]);
|
||||
|
||||
const onSubmit = form.handleSubmit((value) => {
|
||||
testQuery.start(mapInputs(value));
|
||||
});
|
||||
|
||||
@@ -1,38 +1,44 @@
|
||||
import type { CodeSample } from '@/ui/operation';
|
||||
import * as CURL from './curl';
|
||||
import * as JS from './javascript';
|
||||
import * as Go from './go';
|
||||
import * as Python from './python';
|
||||
import * as Java from './java';
|
||||
import * as CSharp from './csharp';
|
||||
import type { CodeUsageGenerator } from '@/ui/operation/api-example';
|
||||
|
||||
export const defaultSamples: CodeSample[] = [
|
||||
export const defaultSamples: CodeUsageGenerator[] = [
|
||||
{
|
||||
id: 'curl',
|
||||
label: 'cURL',
|
||||
source: CURL.generator,
|
||||
lang: 'bash',
|
||||
},
|
||||
{
|
||||
id: 'js',
|
||||
label: 'JavaScript',
|
||||
source: JS.generator,
|
||||
lang: 'js',
|
||||
},
|
||||
{
|
||||
id: 'go',
|
||||
label: 'Go',
|
||||
source: Go.generator,
|
||||
lang: 'go',
|
||||
},
|
||||
{
|
||||
id: 'python',
|
||||
label: 'Python',
|
||||
source: Python.generator,
|
||||
lang: 'python',
|
||||
},
|
||||
{
|
||||
id: 'java',
|
||||
label: 'Java',
|
||||
source: Java.generator,
|
||||
lang: 'java',
|
||||
},
|
||||
{
|
||||
id: 'csharp',
|
||||
label: 'C#',
|
||||
source: CSharp.generator,
|
||||
lang: 'csharp',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { createProxy } from '@/server/proxy';
|
||||
import type { CodeSample } from '@/ui/operation';
|
||||
import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
|
||||
import {
|
||||
processDocument,
|
||||
type ProcessedDocument,
|
||||
} from '@/utils/process-document';
|
||||
import type { CodeUsageGenerator } from '@/ui/operation/api-example';
|
||||
|
||||
/**
|
||||
* schema id -> file path, URL, or downloaded schema object
|
||||
@@ -73,6 +73,14 @@ export function createOpenAPI(options: OpenAPIOptions = {}): OpenAPIServer {
|
||||
};
|
||||
}
|
||||
|
||||
export function createCodeSample<T>(options: CodeSample<T>): CodeSample {
|
||||
return options as CodeSample;
|
||||
export function createCodeSample<T>(
|
||||
options: Partial<CodeUsageGenerator<T>>,
|
||||
): CodeUsageGenerator {
|
||||
const {
|
||||
lang = 'unknown',
|
||||
id = lang,
|
||||
...rest
|
||||
} = options as Partial<CodeUsageGenerator>;
|
||||
|
||||
return { id, lang, ...rest };
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { OpenAPIV3_1 as V3_1 } from 'openapi-types';
|
||||
import type { default as Slugger } from 'github-slugger';
|
||||
import { type Renderer } from '@/ui/renderer';
|
||||
import type { NoReference } from '@/utils/schema';
|
||||
import type { ProcessedDocument } from '@/utils/process-document';
|
||||
import type { MediaAdapter } from '@/requests/media/adapter';
|
||||
import type { OpenAPIOptions } from '@/server';
|
||||
import type { CreateAPIPageOptions } from './ui/api-page';
|
||||
import type { CodeUsageGenerator } from './ui/operation/api-example';
|
||||
|
||||
export type Document = V3_1.Document;
|
||||
export type OperationObject = V3_1.OperationObject;
|
||||
@@ -17,15 +17,18 @@ export type TagObject = V3_1.TagObject;
|
||||
export type ServerObject = V3_1.ServerObject;
|
||||
export type CallbackObject = V3_1.CallbackObject;
|
||||
export type ServerVariableObject = V3_1.ServerVariableObject;
|
||||
export type ResponseObject = V3_1.ResponseObject;
|
||||
|
||||
export type MethodInformation = NoReference<OperationObject> & {
|
||||
method: string;
|
||||
'x-codeSamples'?: CodeUsageGenerator[];
|
||||
'x-selectedCodeSample'?: string;
|
||||
'x-exclusiveCodeSample'?: string;
|
||||
};
|
||||
|
||||
export interface RenderContext
|
||||
extends Pick<OpenAPIOptions, 'proxyUrl'>,
|
||||
CreateAPIPageOptions {
|
||||
renderer: Renderer;
|
||||
servers: NoReference<ServerObject>[];
|
||||
slugger: Slugger;
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import Slugger from 'github-slugger';
|
||||
import { CodeSample, Operation } from '@/ui/operation';
|
||||
import { Operation } from '@/ui/operation';
|
||||
import type { MethodInformation, RenderContext } from '@/types';
|
||||
import { createMethod, NoReference } from '@/utils/schema';
|
||||
import { createRenders } from '@/ui/renderer';
|
||||
import type { OpenAPIV3_1 } from 'openapi-types';
|
||||
import type { ProcessedDocument } from '@/utils/process-document';
|
||||
import { defaultAdapters, MediaAdapter } from '@/requests/media/adapter';
|
||||
import { FC, ReactNode } from 'react';
|
||||
import { ComponentProps, FC, ReactNode } from 'react';
|
||||
import type {
|
||||
HighlightOptionsCommon,
|
||||
HighlightOptionsThemes,
|
||||
} from 'fumadocs-core/highlight';
|
||||
import type { OpenAPIServer } from '@/server';
|
||||
import type { APIPageClientOptions } from './client';
|
||||
import { cn } from 'fumadocs-ui/utils/cn';
|
||||
import type { CodeUsageGenerator, ResponseTab } from './operation/api-example';
|
||||
import { ApiProvider } from './contexts/api';
|
||||
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
@@ -33,9 +35,11 @@ export interface CreateAPIPageOptions {
|
||||
| false;
|
||||
|
||||
/**
|
||||
* Generate code samples for endpoint.
|
||||
* Generate example code usage for endpoints.
|
||||
*/
|
||||
generateCodeSamples?: (method: MethodInformation) => Awaitable<CodeSample[]>;
|
||||
generateCodeSamples?: (
|
||||
method: MethodInformation,
|
||||
) => Awaitable<CodeUsageGenerator[]>;
|
||||
|
||||
shikiOptions?: Omit<HighlightOptionsCommon, 'lang' | 'components'> &
|
||||
HighlightOptionsThemes;
|
||||
@@ -59,6 +63,28 @@ export interface CreateAPIPageOptions {
|
||||
* @defaultValue false
|
||||
*/
|
||||
showExampleInFields?: boolean;
|
||||
|
||||
renderResponseTabs?: (
|
||||
tabs: ResponseTab[],
|
||||
ctx: RenderContext,
|
||||
) => Awaitable<ReactNode>;
|
||||
|
||||
renderAPIExampleLayout?: (
|
||||
slots: {
|
||||
selector: ReactNode;
|
||||
usageTabs: ReactNode;
|
||||
responseTabs: ReactNode;
|
||||
},
|
||||
ctx: RenderContext,
|
||||
) => Awaitable<ReactNode>;
|
||||
|
||||
/**
|
||||
* @param generators - codegens for API example usages
|
||||
*/
|
||||
renderAPIExampleUsageTabs?: (
|
||||
generators: CodeUsageGenerator<unknown>[],
|
||||
ctx: RenderContext,
|
||||
) => Awaitable<ReactNode>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -76,7 +102,7 @@ export interface CreateAPIPageOptions {
|
||||
path: string;
|
||||
method: MethodInformation;
|
||||
ctx: RenderContext;
|
||||
}) => ReactNode | Promise<ReactNode>;
|
||||
}) => Awaitable<ReactNode>;
|
||||
};
|
||||
|
||||
client?: APIPageClientOptions;
|
||||
@@ -135,9 +161,6 @@ export function createAPIPage(
|
||||
schema: processed,
|
||||
proxyUrl: server.options.proxyUrl,
|
||||
showResponseSchema: options.showResponseSchema,
|
||||
renderer: {
|
||||
...createRenders(),
|
||||
},
|
||||
shikiOptions: options.shikiOptions,
|
||||
generateTypeScriptSchema: options.generateTypeScriptSchema,
|
||||
generateCodeSamples: options.generateCodeSamples,
|
||||
@@ -153,6 +176,32 @@ export function createAPIPage(
|
||||
};
|
||||
}
|
||||
|
||||
function Root({
|
||||
children,
|
||||
className,
|
||||
ctx,
|
||||
...props
|
||||
}: { ctx: RenderContext } & ComponentProps<'div'>) {
|
||||
const mediaAdapters: Record<string, MediaAdapter> = {};
|
||||
for (const k in ctx.mediaAdapters) {
|
||||
const adapter = ctx.mediaAdapters[k];
|
||||
|
||||
if (adapter.client) mediaAdapters[k] = adapter.client;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-24 text-sm', className)} {...props}>
|
||||
<ApiProvider
|
||||
mediaAdapters={mediaAdapters}
|
||||
servers={ctx.servers}
|
||||
shikiOptions={ctx.shikiOptions}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function APIPage({
|
||||
hasHead = false,
|
||||
operations,
|
||||
@@ -164,7 +213,7 @@ async function APIPage({
|
||||
const { dereferenced } = ctx.schema;
|
||||
|
||||
return (
|
||||
<ctx.renderer.Root ctx={ctx}>
|
||||
<Root ctx={ctx}>
|
||||
{operations?.map((item) => {
|
||||
const pathItem = dereferenced.paths?.[item.path];
|
||||
if (!pathItem)
|
||||
@@ -216,6 +265,6 @@ async function APIPage({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ctx.renderer.Root>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
'use client';
|
||||
import type { PlaygroundClientOptions } from '@/playground/client';
|
||||
import type { OperationClientOptions } from '../operation/client';
|
||||
|
||||
export interface APIPageClientOptions {
|
||||
playground?: PlaygroundClientOptions;
|
||||
operation?: OperationClientOptions;
|
||||
}
|
||||
|
||||
export function defineClientConfig(
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
'use client';
|
||||
import {
|
||||
createContext,
|
||||
type HTMLAttributes,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useApiContext, useServerSelectContext } from '@/ui/contexts/api';
|
||||
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock';
|
||||
import type { CodeSample } from '@/ui/operation';
|
||||
import type { SamplesProps } from '@/ui/renderer';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/ui/components/select';
|
||||
import {
|
||||
joinURL,
|
||||
resolveRequestData,
|
||||
resolveServerUrl,
|
||||
withBase,
|
||||
} from '@/utils/url';
|
||||
import type { RawRequestData, RequestData } from '@/requests/types';
|
||||
|
||||
type UpdateListener = (data: RawRequestData, encoded: RequestData) => void;
|
||||
|
||||
const CodeExampleContext = createContext<{
|
||||
route: string;
|
||||
getExample: (
|
||||
key: string,
|
||||
) => { data: RawRequestData; encoded: RequestData } | undefined;
|
||||
|
||||
key: string;
|
||||
setKey: (key: string) => void;
|
||||
|
||||
setData: (data: RawRequestData, encoded: RequestData) => void;
|
||||
|
||||
addListener: (listener: UpdateListener) => void;
|
||||
removeListener: (listener: UpdateListener) => void;
|
||||
} | null>(null);
|
||||
|
||||
export function CodeExampleProvider({
|
||||
route,
|
||||
examples,
|
||||
initialKey,
|
||||
children,
|
||||
}: {
|
||||
route: string;
|
||||
examples: {
|
||||
key: string;
|
||||
data: RawRequestData;
|
||||
encoded: RequestData;
|
||||
}[];
|
||||
initialKey?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [key, setKey] = useState(initialKey ?? examples[0].key);
|
||||
const listeners = useRef<UpdateListener[]>([]);
|
||||
const examplesRef = useRef(examples);
|
||||
examplesRef.current = examples;
|
||||
|
||||
return (
|
||||
<CodeExampleContext
|
||||
value={useMemo(
|
||||
() => ({
|
||||
key,
|
||||
route,
|
||||
setKey: (newKey: string) => {
|
||||
const example = examplesRef.current.find(
|
||||
(example) => example.key === newKey,
|
||||
);
|
||||
if (!example) return;
|
||||
|
||||
setKey(newKey);
|
||||
for (const listener of listeners.current) {
|
||||
listener(example.data, example.encoded);
|
||||
}
|
||||
},
|
||||
getExample: (key) => {
|
||||
return examplesRef.current.find((example) => example.key === key);
|
||||
},
|
||||
setData: (data, encoded) => {
|
||||
for (const example of examplesRef.current) {
|
||||
if (example.key === key) {
|
||||
// persistent changes
|
||||
example.data = data;
|
||||
example.encoded = encoded;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const listener of listeners.current) {
|
||||
listener(data, encoded);
|
||||
}
|
||||
},
|
||||
removeListener: (listener) => {
|
||||
listeners.current = listeners.current.filter(
|
||||
(item) => item !== listener,
|
||||
);
|
||||
},
|
||||
addListener: (listener) => {
|
||||
// initial call to listeners to ensure their data is the latest
|
||||
// this is necessary to avoid race conditions between `useEffect()`
|
||||
const example = examplesRef.current.find(
|
||||
(example) => example.key === key,
|
||||
)!;
|
||||
listener(example.data, example.encoded);
|
||||
listeners.current.push(listener);
|
||||
},
|
||||
}),
|
||||
[key, route],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</CodeExampleContext>
|
||||
);
|
||||
}
|
||||
|
||||
export function CodeExample(sample: CodeSample) {
|
||||
const { shikiOptions, mediaAdapters } = useApiContext();
|
||||
const { getExample, key, route, addListener, removeListener } =
|
||||
useContext(CodeExampleContext)!;
|
||||
const { server } = useServerSelectContext();
|
||||
const [data, setData] = useState(() => getExample(key)!.encoded);
|
||||
|
||||
useEffect(() => {
|
||||
const listener: UpdateListener = (_, encoded) => setData(encoded);
|
||||
|
||||
addListener(listener);
|
||||
return () => {
|
||||
removeListener(listener);
|
||||
};
|
||||
}, [addListener, removeListener]);
|
||||
|
||||
const code = useMemo(() => {
|
||||
if (!sample.source) return;
|
||||
if (typeof sample.source === 'string') return sample.source;
|
||||
|
||||
return sample.source(
|
||||
joinURL(
|
||||
withBase(
|
||||
server ? resolveServerUrl(server.url, server.variables) : '/',
|
||||
typeof window !== 'undefined'
|
||||
? window.location.origin
|
||||
: 'https://loading',
|
||||
),
|
||||
resolveRequestData(route, data),
|
||||
),
|
||||
data,
|
||||
{
|
||||
server: sample.serverContext,
|
||||
mediaAdapters,
|
||||
},
|
||||
);
|
||||
}, [mediaAdapters, sample, server, route, data]);
|
||||
|
||||
if (!code || !sample) return null;
|
||||
|
||||
return (
|
||||
<DynamicCodeBlock lang={sample.lang} code={code} options={shikiOptions} />
|
||||
);
|
||||
}
|
||||
|
||||
export function CodeExampleSelector({ items }: SamplesProps) {
|
||||
const { key, setKey } = useContext(CodeExampleContext)!;
|
||||
const item = items.find((item) => item.value === key);
|
||||
|
||||
return (
|
||||
<Select value={key} onValueChange={setKey}>
|
||||
<SelectTrigger className="not-prose mb-2">
|
||||
<SelectValue asChild>
|
||||
{item ? <SelectDisplay item={item} /> : null}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{items.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<SelectDisplay item={item} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectDisplay({
|
||||
item,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement> & {
|
||||
item: SamplesProps['items'][number];
|
||||
}) {
|
||||
return (
|
||||
<div {...props}>
|
||||
<span className="font-medium text-sm">{item.title}</span>
|
||||
<span className="text-fd-muted-foreground">{item.description}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useRequestInitialData() {
|
||||
const { getExample, key } = useContext(CodeExampleContext)!;
|
||||
|
||||
return { key, data: getExample(key)!.data };
|
||||
}
|
||||
|
||||
export function useRequestDataUpdater() {
|
||||
const { setData } = useContext(CodeExampleContext)!;
|
||||
return { setData };
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
'use client';
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
use,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { APIExampleItem } from '@/ui/operation/api-example';
|
||||
import type { RawRequestData, RequestData } from '@/requests/types';
|
||||
import type { OperationClientOptions } from '../operation/client';
|
||||
|
||||
export type ExampleUpdateListener = (
|
||||
data: RawRequestData,
|
||||
encoded: RequestData,
|
||||
) => void;
|
||||
|
||||
const OperationContext = createContext<{
|
||||
route: string;
|
||||
examples: APIExampleItem[];
|
||||
client?: OperationClientOptions;
|
||||
|
||||
example: string;
|
||||
setExample: (id: string) => void;
|
||||
setExampleData: (data: RawRequestData, encoded: RequestData) => void;
|
||||
|
||||
addListener: (listener: ExampleUpdateListener) => void;
|
||||
removeListener: (listener: ExampleUpdateListener) => void;
|
||||
} | null>(null);
|
||||
|
||||
export function OperationProvider({
|
||||
route,
|
||||
examples,
|
||||
defaultExampleId,
|
||||
children,
|
||||
client,
|
||||
}: {
|
||||
route: string;
|
||||
examples: APIExampleItem[];
|
||||
defaultExampleId?: string;
|
||||
client?: OperationClientOptions;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [example, setExample] = useState(
|
||||
() => defaultExampleId ?? examples[0].id,
|
||||
);
|
||||
const listeners = useRef<ExampleUpdateListener[]>([]);
|
||||
|
||||
return (
|
||||
<OperationContext
|
||||
value={useMemo(
|
||||
() => ({
|
||||
example,
|
||||
route,
|
||||
client,
|
||||
setExample: (newKey: string) => {
|
||||
const example = examples.find((example) => example.id === newKey);
|
||||
if (!example) return;
|
||||
|
||||
setExample(newKey);
|
||||
for (const listener of listeners.current) {
|
||||
listener(example.data, example.encoded);
|
||||
}
|
||||
},
|
||||
examples,
|
||||
setExampleData: (data, encoded) => {
|
||||
for (const item of examples) {
|
||||
if (item.id === example) {
|
||||
// persistent changes
|
||||
item.data = data;
|
||||
item.encoded = encoded;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const listener of listeners.current) {
|
||||
listener(data, encoded);
|
||||
}
|
||||
},
|
||||
removeListener: (listener) => {
|
||||
listeners.current = listeners.current.filter(
|
||||
(item) => item !== listener,
|
||||
);
|
||||
},
|
||||
addListener: (listener) => {
|
||||
// initial call to listeners to ensure their data is the latest
|
||||
// this is necessary to avoid race conditions between `useEffect()`
|
||||
const active = examples.find((item) => item.id === example)!;
|
||||
|
||||
listener(active.data, active.encoded);
|
||||
listeners.current.push(listener);
|
||||
},
|
||||
}),
|
||||
[example, route, client, examples],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</OperationContext>
|
||||
);
|
||||
}
|
||||
|
||||
export function useOperationContext() {
|
||||
return use(OperationContext)!;
|
||||
}
|
||||
@@ -1,79 +1 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import type { RootProps } from '@/ui/renderer';
|
||||
import { ApiProvider } from '@/ui/lazy';
|
||||
import { cn } from 'fumadocs-ui/utils/cn';
|
||||
import type { MediaAdapter } from '@/requests/media/adapter';
|
||||
|
||||
export function Root({
|
||||
children,
|
||||
className,
|
||||
ctx,
|
||||
...props
|
||||
}: RootProps & HTMLAttributes<HTMLDivElement>) {
|
||||
const mediaAdapters: Record<string, MediaAdapter> = {};
|
||||
for (const k in ctx.mediaAdapters) {
|
||||
const adapter = ctx.mediaAdapters[k];
|
||||
|
||||
if (adapter.client) mediaAdapters[k] = adapter.client;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-24 text-sm', className)} {...props}>
|
||||
<ApiProvider
|
||||
mediaAdapters={mediaAdapters}
|
||||
servers={ctx.servers}
|
||||
shikiOptions={ctx.shikiOptions}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function APIInfo({
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div className={cn('min-w-0 flex-1', className)} {...props}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function API({ children, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex flex-col gap-x-6 gap-y-4 xl:flex-row xl:items-start',
|
||||
props.className,
|
||||
)}
|
||||
style={
|
||||
{
|
||||
'--fd-api-info-top':
|
||||
'calc(12px + var(--fd-nav-height) + var(--fd-banner-height) + var(--fd-tocnav-height, 0px))',
|
||||
...props.style,
|
||||
} as object
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function APIExample(props: HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'prose-no-margin md:sticky md:top-(--fd-api-info-top) xl:w-[400px]',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export * from '@/ui/api-page';
|
||||
|
||||
@@ -11,34 +11,4 @@ function wrap<Props>(V: ComponentType<Props>) {
|
||||
};
|
||||
}
|
||||
|
||||
export const CodeExampleProvider = wrap(
|
||||
lazy(() =>
|
||||
import('./contexts/code-example').then((mod) => ({
|
||||
default: mod.CodeExampleProvider,
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
export const CodeExample = wrap(
|
||||
lazy(() =>
|
||||
import('./contexts/code-example').then((mod) => ({
|
||||
default: mod.CodeExample,
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
export const CodeExampleSelector = wrap(
|
||||
lazy(() =>
|
||||
import('./contexts/code-example').then((mod) => ({
|
||||
default: mod.CodeExampleSelector,
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
export const ClientLazy = wrap(lazy(() => import('@/playground/client')));
|
||||
|
||||
export const ApiProvider = wrap(
|
||||
lazy(() =>
|
||||
import('./contexts/api').then((mod) => ({ default: mod.ApiProvider })),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,73 +1,107 @@
|
||||
import type { MethodInformation, RenderContext } from '@/types';
|
||||
import { type ReactNode } from 'react';
|
||||
import type { MethodInformation, RenderContext, ResponseObject } from '@/types';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Markdown } from '@/ui/components/server/markdown';
|
||||
import { type CodeSample } from '@/ui/operation/index';
|
||||
import { CodeBlock } from '@/ui/components/server/codeblock';
|
||||
import { CodeExample, CodeExampleProvider } from '@/ui/lazy';
|
||||
import { getPreferredType, type NoReference } from '@/utils/schema';
|
||||
import { getRequestData } from '@/ui/operation/get-request-data';
|
||||
import { sample } from 'openapi-sampler';
|
||||
import type { RawRequestData, RequestData } from '@/requests/types';
|
||||
import type {
|
||||
RawRequestData,
|
||||
RequestData,
|
||||
SampleGenerator,
|
||||
} from '@/requests/types';
|
||||
import { defaultSamples } from '@/requests/generators';
|
||||
import { encodeRequestData } from '@/requests/media/encode';
|
||||
import {
|
||||
AccordionContent,
|
||||
AccordionHeader,
|
||||
AccordionItem,
|
||||
Accordions,
|
||||
AccordionTrigger,
|
||||
} from '../components/accordion';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
import {
|
||||
CodeBlockTab,
|
||||
CodeBlockTabs,
|
||||
CodeBlockTabsList,
|
||||
CodeBlockTabsTrigger,
|
||||
} from 'fumadocs-ui/components/codeblock';
|
||||
import { APIExampleSelector, APIExampleUsageTab } from './client';
|
||||
|
||||
interface CustomProperty {
|
||||
'x-codeSamples'?: CodeSample[];
|
||||
'x-selectedCodeSample'?: string;
|
||||
'x-exclusiveCodeSample'?: string;
|
||||
/**
|
||||
* Generate code example for given programming language
|
||||
*/
|
||||
export interface CodeUsageGenerator<T = unknown> {
|
||||
id: string;
|
||||
lang: string;
|
||||
label?: string;
|
||||
/**
|
||||
* either:
|
||||
* - code
|
||||
* - a function imported from a file with "use client" directive
|
||||
* - false (disabled)
|
||||
*/
|
||||
source?: string | SampleGenerator<T> | false;
|
||||
|
||||
/**
|
||||
* Pass extra context to client-side source generator
|
||||
*/
|
||||
serverContext?: T;
|
||||
}
|
||||
|
||||
interface CodeExampleItem {
|
||||
key: string;
|
||||
|
||||
export interface APIExampleItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
data: RawRequestData;
|
||||
encoded: RequestData;
|
||||
}
|
||||
|
||||
export function APIExampleProvider({
|
||||
examples,
|
||||
method,
|
||||
children,
|
||||
route,
|
||||
}: {
|
||||
examples: CodeExampleItem[];
|
||||
method: MethodInformation & CustomProperty;
|
||||
route: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const exclusiveSampleKey = method['x-exclusiveCodeSample'];
|
||||
export interface ResponseTab {
|
||||
/**
|
||||
* HTTP response code
|
||||
*/
|
||||
code: string;
|
||||
|
||||
return (
|
||||
<CodeExampleProvider
|
||||
initialKey={exclusiveSampleKey}
|
||||
route={route}
|
||||
examples={examples}
|
||||
>
|
||||
{children}
|
||||
</CodeExampleProvider>
|
||||
);
|
||||
response: NoReference<ResponseObject>;
|
||||
/**
|
||||
* media type of response
|
||||
*/
|
||||
mediaType: string | null;
|
||||
|
||||
examples?: {
|
||||
/**
|
||||
* generated/defined example data
|
||||
*/
|
||||
sample: unknown;
|
||||
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* description (in Markdown)
|
||||
*/
|
||||
description?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function getAPIExamples(
|
||||
path: string,
|
||||
method: MethodInformation,
|
||||
ctx: RenderContext,
|
||||
): CodeExampleItem[] {
|
||||
): APIExampleItem[] {
|
||||
const media = method.requestBody
|
||||
? getPreferredType(method.requestBody.content)
|
||||
: null;
|
||||
const bodyOfType = media ? method.requestBody?.content[media] : null;
|
||||
|
||||
if (bodyOfType?.examples) {
|
||||
const result: CodeExampleItem[] = [];
|
||||
const result: APIExampleItem[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(bodyOfType.examples)) {
|
||||
const data = getRequestData(path, method, key, ctx);
|
||||
|
||||
result.push({
|
||||
key,
|
||||
id: key,
|
||||
name: value.summary ?? key,
|
||||
description: value.description,
|
||||
data,
|
||||
@@ -85,7 +119,7 @@ export function getAPIExamples(
|
||||
const data = getRequestData(path, method, null, ctx);
|
||||
return [
|
||||
{
|
||||
key: '_default',
|
||||
id: '_default',
|
||||
name: 'Default',
|
||||
description: bodyOfType?.schema?.description,
|
||||
data,
|
||||
@@ -100,62 +134,74 @@ export function getAPIExamples(
|
||||
|
||||
export async function APIExample({
|
||||
method,
|
||||
examples,
|
||||
ctx,
|
||||
}: {
|
||||
examples: CodeExampleItem[];
|
||||
method: MethodInformation & CustomProperty;
|
||||
method: MethodInformation;
|
||||
ctx: RenderContext;
|
||||
}) {
|
||||
const renderer = ctx.renderer;
|
||||
let { renderAPIExampleUsageTabs, renderAPIExampleLayout } = ctx.content ?? {};
|
||||
|
||||
renderAPIExampleLayout ??= (slots) => {
|
||||
return (
|
||||
<div className="prose-no-margin md:sticky md:top-(--fd-api-info-top) xl:w-[400px]">
|
||||
{slots.selector}
|
||||
{slots.usageTabs}
|
||||
{slots.responseTabs}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderAPIExampleUsageTabs ??= (generators) => {
|
||||
if (generators.length === 0) return null;
|
||||
|
||||
return (
|
||||
<CodeBlockTabs
|
||||
groupId="fumadocs_openapi_requests"
|
||||
defaultValue={generators[0].id}
|
||||
>
|
||||
<CodeBlockTabsList>
|
||||
{generators.map((item) => (
|
||||
<CodeBlockTabsTrigger key={item.id} value={item.id}>
|
||||
{item.label ?? item.lang}
|
||||
</CodeBlockTabsTrigger>
|
||||
))}
|
||||
</CodeBlockTabsList>
|
||||
{generators.map((item) => (
|
||||
<CodeBlockTab key={item.id} value={item.id}>
|
||||
<APIExampleUsageTab {...item} />
|
||||
</CodeBlockTab>
|
||||
))}
|
||||
</CodeBlockTabs>
|
||||
);
|
||||
};
|
||||
|
||||
const generators = dedupe([
|
||||
...defaultSamples,
|
||||
...(ctx.generateCodeSamples ? await ctx.generateCodeSamples(method) : []),
|
||||
...(method['x-codeSamples'] ?? []),
|
||||
]).filter((generator) => generator.source !== false);
|
||||
|
||||
const exclusiveSampleKey = method['x-exclusiveCodeSample'];
|
||||
|
||||
return (
|
||||
<renderer.APIExample>
|
||||
{examples.length > 1 && !exclusiveSampleKey && (
|
||||
<renderer.CodeExampleSelector
|
||||
items={examples.map((sample) => ({
|
||||
title: sample.name,
|
||||
description: sample.description ? (
|
||||
<Markdown text={sample.description} />
|
||||
) : null,
|
||||
value: sample.key,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
{generators.length > 0 && (
|
||||
<renderer.Requests items={generators.map((s) => s.label ?? s.lang)}>
|
||||
{generators.map((generator, i) => (
|
||||
<renderer.Request key={i} name={generator.label ?? generator.lang}>
|
||||
<CodeExample {...generator} />
|
||||
</renderer.Request>
|
||||
))}
|
||||
</renderer.Requests>
|
||||
)}
|
||||
<ResponseTabs operation={method} ctx={ctx} />
|
||||
</renderer.APIExample>
|
||||
return renderAPIExampleLayout(
|
||||
{
|
||||
selector: method['x-exclusiveCodeSample'] ? null : <APIExampleSelector />,
|
||||
usageTabs: await renderAPIExampleUsageTabs(generators, ctx),
|
||||
responseTabs: <ResponseTabs operation={method} ctx={ctx} />,
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove duplicated labels
|
||||
* Remove duplicated ids
|
||||
*/
|
||||
function dedupe(samples: CodeSample[]): CodeSample[] {
|
||||
function dedupe(samples: CodeUsageGenerator[]): CodeUsageGenerator[] {
|
||||
const set = new Set<string>();
|
||||
const out: CodeSample[] = [];
|
||||
const out: CodeUsageGenerator[] = [];
|
||||
|
||||
for (let i = samples.length - 1; i >= 0; i--) {
|
||||
const item = samples[i];
|
||||
if (item.label) {
|
||||
if (set.has(item.label)) continue;
|
||||
set.add(item.label);
|
||||
}
|
||||
if (set.has(item.id)) continue;
|
||||
set.add(item.id);
|
||||
|
||||
out.unshift(item);
|
||||
}
|
||||
@@ -170,64 +216,105 @@ function ResponseTabs({
|
||||
operation: NoReference<MethodInformation>;
|
||||
ctx: RenderContext;
|
||||
}) {
|
||||
const { renderer } = ctx;
|
||||
if (!operation.responses) return null;
|
||||
const tabs: ResponseTab[] = [];
|
||||
|
||||
async function renderResponse(code: string) {
|
||||
const response = operation.responses?.[code];
|
||||
const media = response?.content ? getPreferredType(response.content) : null;
|
||||
const responseOfType = media ? response?.content?.[media] : null;
|
||||
for (const [code, response] of Object.entries(operation.responses)) {
|
||||
const media = response.content ? getPreferredType(response.content) : null;
|
||||
const responseOfType = media ? response.content?.[media] : null;
|
||||
|
||||
const tab: ResponseTab = {
|
||||
code,
|
||||
response,
|
||||
mediaType: media as string | null,
|
||||
};
|
||||
|
||||
if (responseOfType?.examples) {
|
||||
tab.examples ??= [];
|
||||
|
||||
for (const [key, sample] of Object.entries(responseOfType.examples)) {
|
||||
const title = sample?.summary ?? `Example ${key}`;
|
||||
|
||||
tab.examples.push({
|
||||
label: title,
|
||||
sample: sample.value,
|
||||
description: sample?.description,
|
||||
});
|
||||
}
|
||||
} else if (responseOfType?.example || responseOfType?.schema) {
|
||||
tab.examples ??= [];
|
||||
tab.examples.push({
|
||||
label: 'Example',
|
||||
sample:
|
||||
responseOfType.example ?? sample(responseOfType.schema as object),
|
||||
});
|
||||
}
|
||||
|
||||
tabs.push(tab);
|
||||
}
|
||||
const { renderResponseTabs = renderResponseTabsDefault } = ctx.content ?? {};
|
||||
|
||||
return renderResponseTabs(tabs, ctx);
|
||||
}
|
||||
|
||||
function renderResponseTabsDefault(
|
||||
tabs: ResponseTab[],
|
||||
ctx: RenderContext,
|
||||
): ReactNode | Promise<ReactNode> {
|
||||
async function renderResponse(tab: ResponseTab) {
|
||||
const { examples = [] } = tab;
|
||||
|
||||
let slot: ReactNode = 'Empty';
|
||||
if (responseOfType?.examples) {
|
||||
const values: string[] = [];
|
||||
|
||||
const children = Object.entries(responseOfType.examples).map(
|
||||
([key, sample]) => {
|
||||
const title = sample?.summary ?? `Example ${key}`;
|
||||
|
||||
values.push(title);
|
||||
return (
|
||||
<renderer.ResponseType key={key} label={title}>
|
||||
{sample?.description && <Markdown text={sample.description} />}
|
||||
<CodeBlock
|
||||
lang="json"
|
||||
code={JSON.stringify(sample.value, null, 2)}
|
||||
ctx={ctx}
|
||||
/>
|
||||
</renderer.ResponseType>
|
||||
);
|
||||
},
|
||||
if (examples.length > 1) {
|
||||
slot = (
|
||||
<Accordions
|
||||
type="single"
|
||||
className="pt-2"
|
||||
defaultValue={examples[0].label}
|
||||
>
|
||||
{examples.map((example, i) => (
|
||||
<AccordionItem key={i} value={example.label}>
|
||||
<AccordionHeader>
|
||||
<AccordionTrigger>{example.label}</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
<AccordionContent className="prose-no-margin">
|
||||
{example.description && <Markdown text={example.description} />}
|
||||
<CodeBlock
|
||||
lang="json"
|
||||
code={JSON.stringify(example.sample, null, 2)}
|
||||
ctx={ctx}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordions>
|
||||
);
|
||||
} else if (examples.length === 1) {
|
||||
const example = examples[0];
|
||||
|
||||
slot = (
|
||||
<renderer.ResponseTypes defaultValue={values[0]}>
|
||||
{children}
|
||||
</renderer.ResponseTypes>
|
||||
);
|
||||
} else if (responseOfType?.example || responseOfType?.schema) {
|
||||
slot = (
|
||||
<CodeBlock
|
||||
lang="json"
|
||||
code={JSON.stringify(
|
||||
responseOfType.example ?? sample(responseOfType.schema as object),
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
ctx={ctx}
|
||||
/>
|
||||
<>
|
||||
{example.description && <Markdown text={example.description} />}
|
||||
<CodeBlock
|
||||
lang="json"
|
||||
code={JSON.stringify(example.sample, null, 2)}
|
||||
ctx={ctx}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <renderer.Response value={code}>{slot}</renderer.Response>;
|
||||
return <Tab value={tab.code}>{slot}</Tab>;
|
||||
}
|
||||
|
||||
const codes = Object.keys(operation.responses);
|
||||
if (codes.length === 0) return null;
|
||||
if (tabs.length === 0) return null;
|
||||
|
||||
return (
|
||||
<renderer.Responses items={codes}>
|
||||
{codes.map((code) => renderResponse(code))}
|
||||
</renderer.Responses>
|
||||
<Tabs
|
||||
groupId="fumadocs_openapi_responses"
|
||||
items={tabs.map((tab) => tab.code)}
|
||||
>
|
||||
{tabs.map(renderResponse)}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import { Check, Copy } from 'lucide-react';
|
||||
import {
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
type FC,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
@@ -19,6 +21,28 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/ui/components/select';
|
||||
import {
|
||||
type ExampleUpdateListener,
|
||||
useOperationContext,
|
||||
} from '../contexts/operation';
|
||||
import type { CodeUsageGenerator, APIExampleItem } from './api-example';
|
||||
import { useApiContext, useServerSelectContext } from '../contexts/api';
|
||||
import {
|
||||
joinURL,
|
||||
withBase,
|
||||
resolveServerUrl,
|
||||
resolveRequestData,
|
||||
} from '@/utils/url';
|
||||
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock';
|
||||
|
||||
export interface OperationClientOptions {
|
||||
APIExampleSelector?: FC<{
|
||||
items: APIExampleItem[];
|
||||
|
||||
value: string;
|
||||
onValueChange: (id: string) => void;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function CopyResponseTypeScript({ code }: { code: string }) {
|
||||
const [isChecked, onCopy] = useCopyButton(() => {
|
||||
@@ -111,3 +135,94 @@ export function SelectTabTrigger({
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export function APIExampleSelector() {
|
||||
const {
|
||||
example: key,
|
||||
setExample: setKey,
|
||||
examples,
|
||||
client: { APIExampleSelector: Override } = {},
|
||||
} = useOperationContext();
|
||||
|
||||
if (Override) {
|
||||
return <Override items={examples} value={key} onValueChange={setKey} />;
|
||||
}
|
||||
|
||||
function renderItem(item: APIExampleItem) {
|
||||
return (
|
||||
<div>
|
||||
<span className="font-medium text-sm">{item.name}</span>
|
||||
<span className="text-fd-muted-foreground">{item.description}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (examples.length === 1) return null;
|
||||
const selected = examples.find((item) => item.id === key);
|
||||
return (
|
||||
<Select value={key} onValueChange={setKey}>
|
||||
<SelectTrigger className="not-prose mb-2">
|
||||
{selected && <SelectValue asChild>{renderItem(selected)}</SelectValue>}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{examples.map((item) => (
|
||||
<SelectItem key={item.id} value={item.id}>
|
||||
{renderItem(item)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export function APIExampleUsageTab(sample: CodeUsageGenerator) {
|
||||
const { shikiOptions, mediaAdapters } = useApiContext();
|
||||
const {
|
||||
examples,
|
||||
example: key,
|
||||
route,
|
||||
addListener,
|
||||
removeListener,
|
||||
} = useOperationContext();
|
||||
const { server } = useServerSelectContext();
|
||||
const [data, setData] = useState(
|
||||
() => examples.find((example) => example.id === key)!.encoded,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const listener: ExampleUpdateListener = (_, encoded) => setData(encoded);
|
||||
|
||||
addListener(listener);
|
||||
return () => {
|
||||
removeListener(listener);
|
||||
};
|
||||
}, [addListener, removeListener]);
|
||||
|
||||
const code = useMemo(() => {
|
||||
if (!sample.source) return;
|
||||
if (typeof sample.source === 'string') return sample.source;
|
||||
|
||||
return sample.source(
|
||||
joinURL(
|
||||
withBase(
|
||||
server ? resolveServerUrl(server.url, server.variables) : '/',
|
||||
typeof window !== 'undefined'
|
||||
? window.location.origin
|
||||
: 'https://loading',
|
||||
),
|
||||
resolveRequestData(route, data),
|
||||
),
|
||||
data,
|
||||
{
|
||||
server: sample.serverContext,
|
||||
mediaAdapters,
|
||||
},
|
||||
);
|
||||
}, [mediaAdapters, sample, server, route, data]);
|
||||
|
||||
if (!code || !sample) return null;
|
||||
|
||||
return (
|
||||
<DynamicCodeBlock lang={sample.lang} code={code} options={shikiOptions} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,13 +20,8 @@ import { idToTitle } from '@/utils/id-to-title';
|
||||
import { Markdown } from '../components/server/markdown';
|
||||
import { heading } from '../components/server/heading';
|
||||
import { Schema } from '../schema';
|
||||
import {
|
||||
APIExample,
|
||||
APIExampleProvider,
|
||||
getAPIExamples,
|
||||
} from '@/ui/operation/api-example';
|
||||
import { APIExample, getAPIExamples } from '@/ui/operation/api-example';
|
||||
import { MethodLabel } from '@/ui/components/method-label';
|
||||
import { type SampleGenerator } from '@/requests/types';
|
||||
import { getTypescriptSchema } from '@/utils/get-typescript-schema';
|
||||
import {
|
||||
CopyResponseTypeScript,
|
||||
@@ -43,23 +38,8 @@ import {
|
||||
} from '@/ui/components/accordion';
|
||||
import { isMediaTypeSupported } from '@/requests/media/adapter';
|
||||
import { cn } from 'fumadocs-ui/utils/cn';
|
||||
|
||||
export interface CodeSample<T = unknown> {
|
||||
lang: string;
|
||||
label?: string;
|
||||
/**
|
||||
* either:
|
||||
* - code
|
||||
* - a function imported from a file with "use client" directive
|
||||
* - false (disabled)
|
||||
*/
|
||||
source?: string | SampleGenerator<T> | false;
|
||||
|
||||
/**
|
||||
* Pass extra context to client-side source generator
|
||||
*/
|
||||
serverContext?: T;
|
||||
}
|
||||
import { APIPlayground } from '@/playground';
|
||||
import { OperationProvider } from '../contexts/operation';
|
||||
|
||||
const ParamTypes = {
|
||||
path: 'Path Parameters',
|
||||
@@ -258,10 +238,11 @@ export function Operation({
|
||||
|
||||
const playgroundEnabled = ctx.playground?.enabled ?? true;
|
||||
const info = (
|
||||
<ctx.renderer.APIInfo head={headNode} method={method.method} route={path}>
|
||||
<div className="min-w-0 flex-1">
|
||||
{headNode}
|
||||
{type === 'operation' &&
|
||||
(playgroundEnabled ? (
|
||||
<ctx.renderer.APIPlayground path={path} method={method} ctx={ctx} />
|
||||
<APIPlayground path={path} method={method} ctx={ctx} />
|
||||
) : (
|
||||
<div className="flex flex-row items-center gap-2.5 p-3 rounded-xl border bg-fd-card text-fd-card-foreground not-prose">
|
||||
<MethodLabel className="text-xs">{method.method}</MethodLabel>
|
||||
@@ -275,25 +256,53 @@ export function Operation({
|
||||
{bodyNode}
|
||||
{responseNode}
|
||||
{callbacksNode}
|
||||
</ctx.renderer.APIInfo>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (type === 'operation') {
|
||||
const examples = getAPIExamples(path, method, ctx);
|
||||
|
||||
return (
|
||||
<ctx.renderer.API>
|
||||
<APIExampleProvider route={path} examples={examples} method={method}>
|
||||
<OperationContainer>
|
||||
<OperationProvider
|
||||
defaultExampleId={
|
||||
method['x-exclusiveCodeSample'] ?? method['x-selectedCodeSample']
|
||||
}
|
||||
route={path}
|
||||
examples={examples}
|
||||
client={ctx.client?.operation}
|
||||
>
|
||||
{info}
|
||||
<APIExample examples={examples} method={method} ctx={ctx} />
|
||||
</APIExampleProvider>
|
||||
</ctx.renderer.API>
|
||||
<APIExample method={method} ctx={ctx} />
|
||||
</OperationProvider>
|
||||
</OperationContainer>
|
||||
);
|
||||
} else {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
function OperationContainer({ children, ...props }: ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex flex-col gap-x-6 gap-y-4 xl:flex-row xl:items-start',
|
||||
props.className,
|
||||
)}
|
||||
style={
|
||||
{
|
||||
'--fd-api-info-top':
|
||||
'calc(12px + var(--fd-nav-height) + var(--fd-banner-height) + var(--fd-tocnav-height, 0px))',
|
||||
...props.style,
|
||||
} as object
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function ResponseAccordion({
|
||||
status,
|
||||
operation,
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import type { ComponentType, ReactNode } from 'react';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
import {
|
||||
AccordionContent,
|
||||
AccordionHeader,
|
||||
AccordionItem,
|
||||
Accordions,
|
||||
AccordionTrigger,
|
||||
} from '@/ui/components/accordion';
|
||||
import { API, APIExample, APIInfo, Root } from '@/ui';
|
||||
import type { RenderContext } from '@/types';
|
||||
import { APIPlayground, type APIPlaygroundProps } from '@/playground';
|
||||
import { CodeExampleSelector } from '@/ui/lazy';
|
||||
import {
|
||||
CodeBlockTab,
|
||||
CodeBlockTabs,
|
||||
CodeBlockTabsList,
|
||||
CodeBlockTabsTrigger,
|
||||
} from 'fumadocs-ui/components/codeblock';
|
||||
|
||||
export interface ResponsesProps {
|
||||
items: string[];
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export interface ResponseProps {
|
||||
value: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export interface APIInfoProps {
|
||||
method: string;
|
||||
route: string;
|
||||
|
||||
head: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export interface PropertyProps {
|
||||
name: string;
|
||||
type: ReactNode;
|
||||
required?: boolean;
|
||||
deprecated?: boolean;
|
||||
children?: ReactNode;
|
||||
nested?: boolean;
|
||||
}
|
||||
|
||||
export interface ObjectCollapsibleProps {
|
||||
name: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export interface RequestProps {
|
||||
name: string;
|
||||
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export interface SamplesProps {
|
||||
items: {
|
||||
title: string;
|
||||
description?: ReactNode;
|
||||
value: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface ResponseTypeProps {
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export interface RootProps {
|
||||
ctx: RenderContext;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export interface Renderer {
|
||||
Root: ComponentType<RootProps>;
|
||||
API: ComponentType<{ children: ReactNode }>;
|
||||
APIInfo: ComponentType<APIInfoProps>;
|
||||
APIExample: ComponentType<{ children: ReactNode }>;
|
||||
|
||||
Responses: ComponentType<ResponsesProps>;
|
||||
Response: ComponentType<ResponseProps>;
|
||||
CodeExampleSelector: ComponentType<SamplesProps>;
|
||||
Requests: ComponentType<{ items: string[]; children: ReactNode }>;
|
||||
Request: ComponentType<RequestProps>;
|
||||
ResponseTypes: ComponentType<{ defaultValue?: string; children: ReactNode }>;
|
||||
ResponseType: ComponentType<ResponseTypeProps>;
|
||||
|
||||
APIPlayground: ComponentType<APIPlaygroundProps>;
|
||||
}
|
||||
|
||||
export function createRenders(): Renderer {
|
||||
return {
|
||||
Root,
|
||||
API,
|
||||
APIInfo: ({ children, head }) => (
|
||||
<APIInfo>
|
||||
{head}
|
||||
{children}
|
||||
</APIInfo>
|
||||
),
|
||||
APIExample,
|
||||
Responses: (props) => (
|
||||
<Tabs {...props} groupId="fumadocs_openapi_responses" />
|
||||
),
|
||||
Response: Tab,
|
||||
ResponseTypes: (props) => (
|
||||
<Accordions
|
||||
type="single"
|
||||
className="pt-2"
|
||||
defaultValue={props.defaultValue}
|
||||
>
|
||||
{props.children}
|
||||
</Accordions>
|
||||
),
|
||||
ResponseType: (props) => (
|
||||
<AccordionItem value={props.label}>
|
||||
<AccordionHeader>
|
||||
<AccordionTrigger>{props.label}</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
<AccordionContent className="prose-no-margin">
|
||||
{props.children}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
),
|
||||
Requests: ({ items, children }) => (
|
||||
<CodeBlockTabs
|
||||
groupId="fumadocs_openapi_requests"
|
||||
defaultValue={items[0]}
|
||||
>
|
||||
<CodeBlockTabsList>
|
||||
{items.map((item) => (
|
||||
<CodeBlockTabsTrigger key={item} value={item}>
|
||||
{item}
|
||||
</CodeBlockTabsTrigger>
|
||||
))}
|
||||
</CodeBlockTabsList>
|
||||
{children}
|
||||
</CodeBlockTabs>
|
||||
),
|
||||
|
||||
CodeExampleSelector,
|
||||
Request: (props) => (
|
||||
<CodeBlockTab value={props.name}>{props.children}</CodeBlockTab>
|
||||
),
|
||||
APIPlayground,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user