OpenAPI: support servers override

This commit is contained in:
Fuma Nama
2026-02-11 23:12:17 +08:00
parent 162967cac9
commit 013bba77fd
12 changed files with 68 additions and 41 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
support servers override
+1 -1
View File
@@ -58,7 +58,7 @@
"scripts": {
"build": "tsdown --config-loader unrun",
"clean": "rimraf dist",
"dev": "tsdown --watch",
"dev": "tsdown --watch --clean false",
"lint": "eslint .",
"types:check": "tsc --noEmit"
},
+2 -2
View File
@@ -10,7 +10,7 @@ import {
type ComponentProps,
useRef,
} from 'react';
import { useApiContext } from '@/ui/contexts/api';
import { useApiContext, useServerContext } from '@/ui/contexts/api';
import type { FetchResult } from '@/playground/fetcher';
import type { ParameterField, SecurityEntry } from '@/playground/index';
import { getStatusInfo } from './status-info';
@@ -142,7 +142,6 @@ export default function PlaygroundClient({
const storageKeys = useStorageKey();
const {
mediaAdapters,
serverRef,
client: {
playground: {
components: { ResultDisplay = DefaultResultDisplay } = {},
@@ -151,6 +150,7 @@ export default function PlaygroundClient({
} = {},
},
} = useApiContext();
const { serverRef } = useServerContext();
const [securityId, setSecurityId] = useState(0);
const { inputs, mapInputs, initAuthValues } = useAuthInputs(
securities[securityId],
@@ -1,5 +1,5 @@
'use client';
import { useApiContext, useServerSelectContext } from '@/ui/contexts/api';
import { useServerContext, useServerSelectContext } from '@/ui/contexts/api';
import {
Select,
SelectContent,
@@ -24,7 +24,7 @@ import type { NoReference } from '@/utils/schema';
import { StfProvider, useFieldValue, useListener, useStf } from '@fumari/stf';
export default function ServerSelect(props: HTMLAttributes<HTMLDivElement>) {
const { servers } = useApiContext();
const { servers } = useServerContext();
const { server, setServer, setServerVariables } = useServerSelectContext();
const [open, setOpen] = useState(false);
const [isMounted, setIsMounted] = useState(false);
-1
View File
@@ -29,7 +29,6 @@ export type MethodInformation = NoReference<OperationObject> & {
export interface RenderContext
extends Pick<OpenAPIOptions, 'proxyUrl'>, Omit<CreateAPIPageOptions, 'renderMarkdown'> {
servers: NoReference<ServerObject>[];
slugger: Slugger;
/**
+5 -7
View File
@@ -3,7 +3,7 @@ import type { RenderContext } from '@/types';
import { createMethod } from '@/utils/schema';
import type { OpenAPIV3_1 } from 'openapi-types';
import type { ProcessedDocument } from '@/utils/process-document';
import { ApiProviderLazy } from './contexts/api.lazy';
import { ApiProviderLazy, ServerProviderLazy } from './contexts/api.lazy';
export interface ApiPageProps {
document: Promise<ProcessedDocument> | string | ProcessedDocument;
@@ -113,14 +113,12 @@ export async function APIPage({
},
ctx,
);
let servers = ctx.schema.dereferenced.servers;
if (!servers || servers.length === 0) servers = [{ url: '/' }];
return (
<ApiProviderLazy
servers={ctx.servers}
shikiOptions={ctx.shikiOptions}
client={ctx.client ?? {}}
>
{content}
<ApiProviderLazy shikiOptions={ctx.shikiOptions} client={ctx.client ?? {}}>
<ServerProviderLazy servers={servers}>{content}</ServerProviderLazy>
</ApiProviderLazy>
);
}
-7
View File
@@ -211,19 +211,12 @@ export function createAPIPage(
processed = await document;
}
const { dereferenced } = processed;
const servers =
dereferenced.servers && dereferenced.servers.length > 0
? dereferenced.servers
: [{ url: '/' }];
const slugger = new Slugger();
const ctx: RenderContext = {
schema: processed,
proxyUrl: server.options.proxyUrl,
...options,
servers,
mediaAdapters: {
...defaultAdapters,
...options.mediaAdapters,
@@ -4,3 +4,7 @@ import { wrapLazy } from '../../utils/lazy';
export const ApiProviderLazy = wrapLazy(() =>
import('./api').then((mod) => ({ default: mod.ApiProvider })),
);
export const ServerProviderLazy = wrapLazy(() =>
import('./api').then((mod) => ({ default: mod.ServerProvider })),
);
+40 -18
View File
@@ -13,28 +13,38 @@ import type { RenderContext, ServerObject } from '@/types';
import { defaultAdapters, type MediaAdapter } from '@/requests/media/adapter';
import type { NoReference } from '@/utils/schema';
import { useStorageKey } from '../client/storage-key';
import type { APIPageClientOptions } from '../client';
type InheritFromContext = Pick<Required<RenderContext>, 'servers' | 'client'> &
Pick<RenderContext, 'shikiOptions'>;
interface InheritFromContext extends Pick<RenderContext, 'shikiOptions'> {
client: APIPageClientOptions;
}
export interface ApiProviderProps extends InheritFromContext {
export interface ServerProviderProps {
/**
* Base URL for API requests
*/
defaultBaseUrl?: string;
servers: NoReference<ServerObject>[];
}
interface ServerContextType extends ServerProviderProps {
/**
* ref to selected API server (to query)
*/
serverRef: RefObject<SelectedServer | null>;
}
const ServerContext = createContext<ServerContextType | null>(null);
export type ApiProviderProps = InheritFromContext;
export interface SelectedServer {
url: string;
variables: Record<string, string>;
}
interface ApiContextType extends InheritFromContext {
/**
* ref to selected API server (to query)
*/
serverRef: RefObject<SelectedServer | null>;
mediaAdapters: Record<string, MediaAdapter>;
}
@@ -54,6 +64,10 @@ export function useApiContext(): ApiContextType {
return ctx;
}
export function useServerContext() {
return use(ServerContext)!;
}
export function useServerSelectContext(): ServerSelectType {
const ctx = use(ServerSelectContext);
if (!ctx) throw new Error('Component must be used under <ApiProvider />');
@@ -62,42 +76,50 @@ export function useServerSelectContext(): ServerSelectType {
}
export function ApiProvider({
defaultBaseUrl,
children,
servers,
shikiOptions,
client,
}: ApiProviderProps & { children: ReactNode }) {
const serverRef = useRef<SelectedServer | null>(null);
return (
<ApiContext
value={useMemo(
() => ({
serverRef,
shikiOptions,
client,
mediaAdapters: {
...defaultAdapters,
...client.mediaAdapters,
},
servers,
}),
[servers, client, shikiOptions],
[client, shikiOptions],
)}
>
<ServerSelectProvider defaultBaseUrl={defaultBaseUrl}>{children}</ServerSelectProvider>
{children}
</ApiContext>
);
}
export function ServerProvider({
servers,
defaultBaseUrl,
children,
}: ServerProviderProps & { children: ReactNode }) {
const serverRef = useRef<SelectedServer | null>(null);
return (
<ServerContext value={useMemo(() => ({ servers, serverRef }), [servers])}>
<ServerSelectProvider defaultBaseUrl={defaultBaseUrl}>{children}</ServerSelectProvider>
</ServerContext>
);
}
function ServerSelectProvider({
defaultBaseUrl,
children,
}: Pick<ApiProviderProps, 'defaultBaseUrl'> & {
}: Pick<ServerProviderProps, 'defaultBaseUrl'> & {
children: ReactNode;
}) {
const { servers, serverRef } = useApiContext();
const { servers, serverRef } = use(ServerContext)!;
const storageKeys = useStorageKey();
const [server, setServer] = useState<SelectedServer | null>(() => {
const defaultItem = defaultBaseUrl
+8 -2
View File
@@ -23,6 +23,7 @@ import { cn } from '@/utils/cn';
import { APIPlayground } from '@/playground';
import { getExampleRequests, RequestTabs } from './request-tabs';
import { UsageTabsProviderLazy } from './usage-tabs/lazy';
import { ServerProviderLazy } from '../contexts/api.lazy';
const ParamTypes = {
path: 'Path Parameters',
@@ -263,7 +264,7 @@ export async function Operation({
};
const playgroundEnabled = ctx.playground?.enabled ?? true;
const content = await renderOperationLayout(
let content = await renderOperationLayout(
{
header: headNode,
description: descriptionNode,
@@ -288,7 +289,7 @@ export async function Operation({
method,
);
return (
content = (
<UsageTabsProviderLazy
defaultExampleId={method['x-exclusiveCodeSample'] ?? method['x-selectedCodeSample']}
route={path}
@@ -297,6 +298,11 @@ export async function Operation({
{content}
</UsageTabsProviderLazy>
);
if (method.servers) {
content = <ServerProviderLazy servers={method.servers}>{content}</ServerProviderLazy>;
}
return content;
} else {
renderWebhookLayout ??= (slots) => (
<div className="flex flex-col-reverse gap-x-6 gap-y-4 @4xl:flex-row @4xl:items-start">
+1
View File
@@ -54,6 +54,7 @@ export function createMethod(
description: path.description,
summary: path.summary,
...operation,
servers: operation.servers ?? path.servers,
parameters: [...(operation.parameters ?? []), ...(path.parameters ?? [])],
method: method.toUpperCase(),
};
-1
View File
@@ -19,7 +19,6 @@ export async function renderContextFrom(input: string): Promise<RenderContext> {
return text;
},
schema,
servers: schema.dereferenced.servers!,
slugger: new Slugger(),
};
}