OpenAPI: Make Next.js dependency optional

This commit is contained in:
Fuma Nama
2025-08-01 16:20:00 +08:00
parent 4f8f1d6bb6
commit 224f25970a
10 changed files with 64 additions and 61 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Make Next.js dependency optional
-2
View File
@@ -82,7 +82,6 @@
"@types/react": "^19.1.9",
"eslint-config-custom": "workspace:*",
"json-schema-typed": "^8.0.1",
"next": "15.4.5",
"openapi-types": "^12.1.3",
"tailwindcss": "^4.1.11",
"tsc-alias": "^1.8.16",
@@ -91,7 +90,6 @@
"peerDependencies": {
"@scalar/api-client-react": "*",
"@types/react": "*",
"next": "14.x.x || 15.x.x",
"react": "18.x.x || 19.x.x",
"react-dom": "18.x.x || 19.x.x"
},
+2 -2
View File
@@ -1,7 +1,7 @@
import type { MethodInformation, RenderContext } from '@/types';
import dynamic from 'next/dynamic';
import { lazy } from 'react';
const Client = dynamic(() => import('./client'));
const Client = lazy(() => import('./client'));
export function APIPlayground({
path,
+7 -12
View File
@@ -1,9 +1,7 @@
import type { NextRequest } from 'next/server';
const keys = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'] as const;
type Proxy = {
[K in (typeof keys)[number]]: (req: NextRequest) => Promise<Response>;
[K in (typeof keys)[number]]: (req: Request) => Promise<Response>;
};
interface CreateProxyOptions {
@@ -48,26 +46,23 @@ export function createProxy(options: CreateProxyOptions = {}): Proxy {
} = options;
const handlers: Partial<Proxy> = {};
async function handler(req: NextRequest): Promise<Response> {
const url = req.nextUrl.searchParams.get('url');
async function handler(req: Request): Promise<Response> {
const searchParams = new URL(req.url).searchParams;
const url = searchParams.get('url');
if (!url) {
if (!url)
return Response.json(
'[Proxy] A `url` query parameter is required for proxy url',
{
status: 400,
},
);
}
let parsedUrl;
try {
parsedUrl = new URL(url);
} catch {
const parsedUrl = URL.parse(url);
if (!parsedUrl)
return Response.json('[Proxy] Invalid `url` parameter value.', {
status: 400,
});
}
if (allowedOrigins && !allowedOrigins.includes(parsedUrl.origin)) {
return Response.json(
+1
View File
@@ -21,6 +21,7 @@ export type PathItemObject = V3_1.PathItemObject;
export type TagObject = V3_1.TagObject;
export type ServerObject = NoReference<V3_1.ServerObject>;
export type CallbackObject = NoReference<V3_1.CallbackObject>;
export type ServerVariableObject = NoReference<V3_1.ServerVariableObject>;
export type MethodInformation = NoReference<OperationObject> & {
method: string;
+16 -10
View File
@@ -1,18 +1,24 @@
'use client';
import dynamic from 'next/dynamic';
import { lazy } from 'react';
export const CodeExampleProvider = dynamic(() =>
import('./contexts/code-example').then((mod) => mod.CodeExampleProvider),
export const CodeExampleProvider = lazy(() =>
import('./contexts/code-example').then((mod) => ({
default: mod.CodeExampleProvider,
})),
);
export const CodeExample = dynamic(() =>
import('./contexts/code-example').then((mod) => mod.CodeExample),
export const CodeExample = lazy(() =>
import('./contexts/code-example').then((mod) => ({
default: mod.CodeExample,
})),
);
export const CodeExampleSelector = dynamic(() =>
import('./contexts/code-example').then((mod) => mod.CodeExampleSelector),
export const CodeExampleSelector = lazy(() =>
import('./contexts/code-example').then((mod) => ({
default: mod.CodeExampleSelector,
})),
);
export const ClientLazy = dynamic(() => import('@/playground/client'));
export const ClientLazy = lazy(() => import('@/playground/client'));
export const ApiProvider = dynamic(() =>
import('./contexts/api').then((mod) => mod.ApiProvider),
export const ApiProvider = lazy(() =>
import('./contexts/api').then((mod) => ({ default: mod.ApiProvider })),
);
+4 -4
View File
@@ -3,7 +3,7 @@ import {
type ComponentProps,
createContext,
type ReactNode,
use,
useContext,
useMemo,
useState,
} from 'react';
@@ -43,8 +43,8 @@ export function SelectTab({
}: ComponentProps<'div'> & {
value: string;
}) {
const { type } = use(Context)!;
if (value !== type) return;
const ctx = useContext(Context);
if (value !== ctx?.type) return;
return <div {...props}>{props.children}</div>;
}
@@ -53,7 +53,7 @@ export function SelectTabTrigger({
items,
...props
}: ComponentProps<typeof SelectTrigger> & { items: string[] }) {
const { type, setType } = use(Context)!;
const { type, setType } = useContext(Context)!;
return (
<Select value={type ?? ''} onValueChange={setType}>
+27 -26
View File
@@ -1,9 +1,5 @@
'use client';
import {
type SelectedServer,
useApiContext,
useServerSelectContext,
} from '@/ui/contexts/api';
import { useApiContext, useServerSelectContext } from '@/ui/contexts/api';
import {
Select,
SelectContent,
@@ -24,12 +20,12 @@ import {
} from '@/ui/components/dialog';
import { resolveServerUrl, withBase } from '@/utils/url';
import { FormProvider, useController, useForm } from 'react-hook-form';
import type { OpenAPIV3_1 } from 'openapi-types';
import { useEffectEvent } from 'fumadocs-core/utils/use-effect-event';
import type { ServerVariableObject } from '@/types';
export default function ServerSelect(props: HTMLAttributes<HTMLDivElement>) {
const { servers } = useApiContext();
const { server, setServer } = useServerSelectContext();
const { server, setServer, setServerVariables } = useServerSelectContext();
const [open, setOpen] = useState(false);
const [isMounted, setIsMounted] = useState(false);
@@ -38,6 +34,9 @@ export default function ServerSelect(props: HTMLAttributes<HTMLDivElement>) {
}, []);
if (servers.length <= 0) return;
const serverSchema = server
? servers.find((obj) => obj.url === server.url)
: null;
return (
<Dialog open={open} onOpenChange={setOpen}>
@@ -69,26 +68,33 @@ export default function ServerSelect(props: HTMLAttributes<HTMLDivElement>) {
))}
</SelectContent>
</Select>
{server && server.variables && (
<ServerSelectContent key={server.url} server={server} />
{server?.variables && serverSchema?.variables && (
<ServerSelectContent
key={server.url}
defaultValues={server.variables}
schema={serverSchema.variables}
onChange={setServerVariables}
/>
)}
</DialogContent>
</Dialog>
);
}
function ServerSelectContent({ server }: { server: SelectedServer }) {
const { servers } = useApiContext();
const { setServerVariables } = useServerSelectContext();
const schema = servers.find((item) => item.url === server.url);
function ServerSelectContent({
defaultValues,
onChange,
schema,
}: {
defaultValues: Record<string, string>;
onChange: (values: Record<string, string>) => void;
schema: Record<string, ServerVariableObject>;
}) {
const form = useForm({
defaultValues: server.variables,
});
const onChangeDebounced = useEffectEvent((values: Record<string, string>) => {
setServerVariables(values);
defaultValues,
});
const onChangeDebounced = useEffectEvent(onChange);
useEffect(() => {
let timer: number | null = null;
@@ -105,12 +111,10 @@ function ServerSelectContent({ server }: { server: SelectedServer }) {
// eslint-disable-next-line react-hooks/exhaustive-deps -- `form` shouldn't be included
}, []);
if (!schema?.variables) return;
return (
<FormProvider {...form}>
<div className="flex flex-col gap-4">
{Object.entries(schema.variables).map(([key, variable]) => {
{Object.entries(schema).map(([key, variable]) => {
return (
<fieldset key={key} className="flex flex-col gap-1">
<label className={cn(labelVariants())} htmlFor={key}>
@@ -119,10 +123,7 @@ function ServerSelectContent({ server }: { server: SelectedServer }) {
<p className="text-xs text-fd-muted-foreground empty:hidden">
{variable.description}
</p>
<Field
fieldName={key}
variable={variable as OpenAPIV3_1.ServerVariableObject}
/>
<Field fieldName={key} variable={variable} />
</fieldset>
);
})}
@@ -135,7 +136,7 @@ function Field({
fieldName,
variable,
}: {
variable: OpenAPIV3_1.ServerVariableObject;
variable: ServerVariableObject;
fieldName: string;
}) {
const { field } = useController({
+2 -2
View File
@@ -1,5 +1,5 @@
import { type JSONSchema } from 'json-schema-typed/draft-2020-12';
import { ReferenceObject } from '@/types';
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
import type { ReferenceObject } from '@/types';
export type NoReference<T> = T extends (infer I)[]
? NoReference<I>[]
-3
View File
@@ -1348,9 +1348,6 @@ importers:
json-schema-typed:
specifier: ^8.0.1
version: 8.0.1
next:
specifier: 15.4.5
version: 15.4.5(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
openapi-types:
specifier: ^12.1.3
version: 12.1.3