fumadocs-openapi 5.9.0 → 5.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +5 -0
- package/dist/server/index.d.ts +15 -1
- package/dist/server/index.js +70 -6
- package/dist/ui/{client-client-DopF7PSu.js → client-client-DTw64k7r.js} +2 -2
- package/dist/ui/fetcher-BuBL-ccr.js +138 -0
- package/dist/ui/{playground-client-HUgWUI95.js → index-client-Br05jF3h.js} +97 -186
- package/dist/ui/index.d.ts +7 -2
- package/dist/ui/index.js +4 -4
- package/package.json +6 -6
package/dist/index.d.ts
CHANGED
|
@@ -58,6 +58,7 @@ interface APIPlaygroundProps {
|
|
|
58
58
|
header?: PrimitiveRequestField[];
|
|
59
59
|
body?: RequestSchema;
|
|
60
60
|
schemas: Record<string, RequestSchema>;
|
|
61
|
+
proxyUrl?: string;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
interface ResponsesProps {
|
|
@@ -191,6 +192,10 @@ type Awaitable<T> = T | Promise<T>;
|
|
|
191
192
|
*/
|
|
192
193
|
type DereferenceMap = Map<unknown, string>;
|
|
193
194
|
interface RenderContext {
|
|
195
|
+
/**
|
|
196
|
+
* The url of proxy to avoid CORS issues
|
|
197
|
+
*/
|
|
198
|
+
proxyUrl?: string;
|
|
194
199
|
renderer: Renderer;
|
|
195
200
|
/**
|
|
196
201
|
* dereferenced schema
|
package/dist/server/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ComponentType, ReactNode, FC } from 'react';
|
|
|
2
2
|
import { OpenAPIV3_1, OpenAPIV3 } from 'openapi-types';
|
|
3
3
|
import Slugger from 'github-slugger';
|
|
4
4
|
import { CodeToHastOptionsCommon, CodeOptionsThemes, BuiltinTheme } from 'shiki';
|
|
5
|
+
import { NextRequest } from 'next/server';
|
|
5
6
|
import { BuildPageTreeOptions } from 'fumadocs-core/source';
|
|
6
7
|
|
|
7
8
|
interface BaseRequestField {
|
|
@@ -59,6 +60,7 @@ interface APIPlaygroundProps {
|
|
|
59
60
|
header?: PrimitiveRequestField[];
|
|
60
61
|
body?: RequestSchema;
|
|
61
62
|
schemas: Record<string, RequestSchema>;
|
|
63
|
+
proxyUrl?: string;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
interface ResponsesProps {
|
|
@@ -177,6 +179,10 @@ type Awaitable<T> = T | Promise<T>;
|
|
|
177
179
|
*/
|
|
178
180
|
type DereferenceMap = Map<unknown, string>;
|
|
179
181
|
interface RenderContext {
|
|
182
|
+
/**
|
|
183
|
+
* The url of proxy to avoid CORS issues
|
|
184
|
+
*/
|
|
185
|
+
proxyUrl?: string;
|
|
180
186
|
renderer: Renderer;
|
|
181
187
|
/**
|
|
182
188
|
* dereferenced schema
|
|
@@ -204,7 +210,8 @@ interface RenderContext {
|
|
|
204
210
|
|
|
205
211
|
type DocumentInput = string | OpenAPIV3_1.Document | OpenAPIV3.Document;
|
|
206
212
|
|
|
207
|
-
|
|
213
|
+
type ApiPageContextProps = Pick<Partial<RenderContext>, 'shikiOptions' | 'generateTypeScriptSchema' | 'generateCodeSamples' | 'proxyUrl'>;
|
|
214
|
+
interface ApiPageProps extends ApiPageContextProps {
|
|
208
215
|
document: DocumentInput;
|
|
209
216
|
hasHead: boolean;
|
|
210
217
|
renderer?: Partial<Renderer>;
|
|
@@ -227,6 +234,12 @@ interface OperationItem {
|
|
|
227
234
|
method: OpenAPIV3_1.HttpMethods;
|
|
228
235
|
}
|
|
229
236
|
|
|
237
|
+
declare const keys: readonly ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"];
|
|
238
|
+
type Proxy = {
|
|
239
|
+
[K in (typeof keys)[number]]: (req: NextRequest) => Promise<Response>;
|
|
240
|
+
};
|
|
241
|
+
declare function createProxy(allowedUrls?: string[]): Proxy;
|
|
242
|
+
|
|
230
243
|
interface OpenAPIOptions extends Omit<Partial<ApiPageProps>, 'document'> {
|
|
231
244
|
/**
|
|
232
245
|
* @deprecated Pass document to `APIPage` instead
|
|
@@ -235,6 +248,7 @@ interface OpenAPIOptions extends Omit<Partial<ApiPageProps>, 'document'> {
|
|
|
235
248
|
}
|
|
236
249
|
interface OpenAPIServer {
|
|
237
250
|
APIPage: FC<ApiPageProps>;
|
|
251
|
+
createProxy: typeof createProxy;
|
|
238
252
|
}
|
|
239
253
|
declare function createOpenAPI(options?: OpenAPIOptions): OpenAPIServer;
|
|
240
254
|
|
package/dist/server/index.js
CHANGED
|
@@ -302,13 +302,14 @@ function Playground({ path, method, ctx }) {
|
|
|
302
302
|
const mediaType = bodyContent ? getPreferredType(bodyContent) : undefined;
|
|
303
303
|
const context = {
|
|
304
304
|
allowFile: mediaType === 'multipart/form-data',
|
|
305
|
-
|
|
305
|
+
references: {},
|
|
306
306
|
nextId () {
|
|
307
307
|
return String(currentId++);
|
|
308
308
|
},
|
|
309
309
|
registered: new WeakMap(),
|
|
310
310
|
render: ctx
|
|
311
311
|
};
|
|
312
|
+
const bodySchema = bodyContent && mediaType && bodyContent[mediaType].schema ? toSchema(bodyContent[mediaType].schema, true, context) : undefined;
|
|
312
313
|
const props = {
|
|
313
314
|
authorization: getAuthorizationField(method, ctx),
|
|
314
315
|
method: method.method,
|
|
@@ -317,8 +318,9 @@ function Playground({ path, method, ctx }) {
|
|
|
317
318
|
path: method.parameters?.filter((v)=>v.in === 'path').map((v)=>parameterToField(v, context)),
|
|
318
319
|
query: method.parameters?.filter((v)=>v.in === 'query').map((v)=>parameterToField(v, context)),
|
|
319
320
|
header: method.parameters?.filter((v)=>v.in === 'header').map((v)=>parameterToField(v, context)),
|
|
320
|
-
body:
|
|
321
|
-
schemas: context.
|
|
321
|
+
body: bodySchema,
|
|
322
|
+
schemas: context.references,
|
|
323
|
+
proxyUrl: ctx.proxyUrl
|
|
322
324
|
};
|
|
323
325
|
return /*#__PURE__*/ jsx(ctx.renderer.APIPlayground, {
|
|
324
326
|
...props
|
|
@@ -344,7 +346,7 @@ function getIdFromSchema(schema, required, ctx) {
|
|
|
344
346
|
if (registered === undefined) {
|
|
345
347
|
const id = ctx.nextId();
|
|
346
348
|
ctx.registered.set(schema, id);
|
|
347
|
-
ctx.
|
|
349
|
+
ctx.references[id] = toSchema(schema, required, ctx);
|
|
348
350
|
return id;
|
|
349
351
|
}
|
|
350
352
|
return registered;
|
|
@@ -553,7 +555,7 @@ function heading(depth, child, ctx) {
|
|
|
553
555
|
return result;
|
|
554
556
|
}
|
|
555
557
|
|
|
556
|
-
const keys = {
|
|
558
|
+
const keys$1 = {
|
|
557
559
|
default: 'Default',
|
|
558
560
|
minimum: 'Minimum',
|
|
559
561
|
maximum: 'Maximum',
|
|
@@ -617,7 +619,7 @@ function Schema({ name, schema, ctx }) {
|
|
|
617
619
|
text: schema.description
|
|
618
620
|
}, "description"));
|
|
619
621
|
const fields = [];
|
|
620
|
-
for (const [key, value] of Object.entries(keys)){
|
|
622
|
+
for (const [key, value] of Object.entries(keys$1)){
|
|
621
623
|
if (key in schema) {
|
|
622
624
|
fields.push({
|
|
623
625
|
key: value,
|
|
@@ -1262,6 +1264,7 @@ async function getContext({ document, dereferenceMap }, options = {}) {
|
|
|
1262
1264
|
return {
|
|
1263
1265
|
document: document,
|
|
1264
1266
|
dereferenceMap,
|
|
1267
|
+
proxyUrl: options.proxyUrl,
|
|
1265
1268
|
renderer: {
|
|
1266
1269
|
...createRenders(options.shikiOptions),
|
|
1267
1270
|
...options.renderer
|
|
@@ -1275,8 +1278,69 @@ async function getContext({ document, dereferenceMap }, options = {}) {
|
|
|
1275
1278
|
};
|
|
1276
1279
|
}
|
|
1277
1280
|
|
|
1281
|
+
const keys = [
|
|
1282
|
+
'GET',
|
|
1283
|
+
'POST',
|
|
1284
|
+
'PUT',
|
|
1285
|
+
'DELETE',
|
|
1286
|
+
'PATCH',
|
|
1287
|
+
'HEAD'
|
|
1288
|
+
];
|
|
1289
|
+
function createProxy(allowedUrls) {
|
|
1290
|
+
const handlers = {};
|
|
1291
|
+
async function handler(req) {
|
|
1292
|
+
const url = req.nextUrl.searchParams.get('url');
|
|
1293
|
+
if (!url) {
|
|
1294
|
+
return Response.json('A `url` query parameter is required for proxy url', {
|
|
1295
|
+
status: 400
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
if (allowedUrls && allowedUrls.every((allowedUrl)=>!allowedUrl.startsWith(url))) {
|
|
1299
|
+
return Response.json('The given `url` query parameter is not allowed', {
|
|
1300
|
+
status: 400
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
const clonedReq = new Request(url, {
|
|
1304
|
+
...req,
|
|
1305
|
+
cache: 'no-cache',
|
|
1306
|
+
mode: 'cors'
|
|
1307
|
+
});
|
|
1308
|
+
clonedReq.headers.forEach((_value, originalKey)=>{
|
|
1309
|
+
const key = originalKey.toLowerCase();
|
|
1310
|
+
const notAllowed = key === 'origin';
|
|
1311
|
+
if (notAllowed) {
|
|
1312
|
+
clonedReq.headers.delete(originalKey);
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
const res = await fetch(clonedReq).catch((e)=>new Error(e.toString()));
|
|
1316
|
+
if (res instanceof Error) {
|
|
1317
|
+
return Response.json(`Failed to proxy request: ${res.message}`, {
|
|
1318
|
+
status: 400
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
const headers = new Headers(res.headers);
|
|
1322
|
+
headers.forEach((_value, originalKey)=>{
|
|
1323
|
+
const key = originalKey.toLowerCase();
|
|
1324
|
+
const notAllowed = key.startsWith('access-control-') || key === 'content-encoding';
|
|
1325
|
+
if (notAllowed) {
|
|
1326
|
+
headers.delete(originalKey);
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
headers.set('X-Forwarded-Host', res.url);
|
|
1330
|
+
return new Response(res.body, {
|
|
1331
|
+
...res,
|
|
1332
|
+
headers
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
for (const key of keys){
|
|
1336
|
+
handlers[key] = handler;
|
|
1337
|
+
}
|
|
1338
|
+
return handlers;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1278
1341
|
function createOpenAPI(options = {}) {
|
|
1279
1342
|
return {
|
|
1343
|
+
createProxy,
|
|
1280
1344
|
APIPage (props) {
|
|
1281
1345
|
return /*#__PURE__*/ jsx(APIPage, {
|
|
1282
1346
|
...options,
|
|
@@ -253,7 +253,7 @@ function useSchemaContext() {
|
|
|
253
253
|
return ctx;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
const APIPlayground = dynamic(()=>import('./
|
|
256
|
+
const APIPlayground = dynamic(()=>import('./index-client-Br05jF3h.js').then(function (n) { return n.i; }).then((mod)=>mod.APIPlayground));
|
|
257
257
|
function Root({ children, baseUrl, className, shikiOptions, ...props }) {
|
|
258
258
|
return /*#__PURE__*/ jsx("div", {
|
|
259
259
|
className: cn('flex flex-col gap-24 text-sm text-fd-muted-foreground', className),
|
|
@@ -309,4 +309,4 @@ function BaseUrlSelect({ baseUrls }) {
|
|
|
309
309
|
});
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
export { APIPlayground as A, BaseUrlSelect as B,
|
|
312
|
+
export { APIPlayground as A, BaseUrlSelect as B, ChevronDown as C, Plus as P, Root as R, SchemaContext as S, Trash2 as T, ChevronUp as a, Check as b, useApiContext as c, CircleCheck as d, CircleX as e, CopyRouteButton as f, useSchemaContext as u };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { r as resolve } from './index-client-Br05jF3h.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param bodySchema - schema of body
|
|
5
|
+
* @param references - defined references of schemas, needed for resolve cyclic references
|
|
6
|
+
*/ function createBrowserFetcher(bodySchema, references) {
|
|
7
|
+
return {
|
|
8
|
+
async fetch (input) {
|
|
9
|
+
const headers = new Headers();
|
|
10
|
+
if (input.type !== 'form-data') headers.append('Content-Type', 'application/json');
|
|
11
|
+
for (const key of Object.keys(input.header)){
|
|
12
|
+
const paramValue = input.header[key];
|
|
13
|
+
if (typeof paramValue === 'string' && paramValue.length > 0) headers.append(key, paramValue.toString());
|
|
14
|
+
}
|
|
15
|
+
return fetch(input.url, {
|
|
16
|
+
method: input.method,
|
|
17
|
+
cache: 'no-cache',
|
|
18
|
+
headers,
|
|
19
|
+
body: bodySchema ? createBodyFromValue(input.type, input.body, bodySchema, references, input.dynamicFields ?? new Map()) : undefined,
|
|
20
|
+
signal: AbortSignal.timeout(10 * 1000)
|
|
21
|
+
}).then(async (res)=>{
|
|
22
|
+
const contentType = res.headers.get('Content-Type') ?? '';
|
|
23
|
+
let type;
|
|
24
|
+
let data;
|
|
25
|
+
if (contentType.startsWith('application/json')) {
|
|
26
|
+
type = 'json';
|
|
27
|
+
data = await res.json();
|
|
28
|
+
} else {
|
|
29
|
+
type = contentType.startsWith('text/html') ? 'html' : 'text';
|
|
30
|
+
data = await res.text();
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
status: res.status,
|
|
34
|
+
type,
|
|
35
|
+
data
|
|
36
|
+
};
|
|
37
|
+
}).catch((e)=>{
|
|
38
|
+
const message = e instanceof Error ? `[${e.name}] ${e.message}` : e.toString();
|
|
39
|
+
return {
|
|
40
|
+
status: 400,
|
|
41
|
+
type: 'text',
|
|
42
|
+
data: `Client side error: ${message}`
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create request body from value
|
|
50
|
+
*/ function createBodyFromValue(type, value, schema, references, dynamicFields) {
|
|
51
|
+
const result = convertValue('body', value, schema, references, dynamicFields);
|
|
52
|
+
if (type === 'json') {
|
|
53
|
+
return JSON.stringify(result);
|
|
54
|
+
}
|
|
55
|
+
const formData = new FormData();
|
|
56
|
+
if (typeof result !== 'object' || !result) {
|
|
57
|
+
throw new Error(`Unsupported body type: ${typeof result}, expected: object`);
|
|
58
|
+
}
|
|
59
|
+
for (const key of Object.keys(result)){
|
|
60
|
+
const prop = result[key];
|
|
61
|
+
if (typeof prop === 'object' && prop instanceof File) {
|
|
62
|
+
formData.set(key, prop);
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(prop) && prop.every((item)=>item instanceof File)) {
|
|
65
|
+
for (const item of prop){
|
|
66
|
+
formData.append(key, item);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (prop && !(prop instanceof File)) {
|
|
70
|
+
formData.set(key, JSON.stringify(prop));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return formData;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Convert a value (object or string) to the corresponding type of schema
|
|
77
|
+
*
|
|
78
|
+
* @param fieldName - field name of value
|
|
79
|
+
* @param value - the original value
|
|
80
|
+
* @param schema - the schema of field
|
|
81
|
+
* @param references - schema references
|
|
82
|
+
* @param dynamicFields - Dynamic references
|
|
83
|
+
*/ function convertValue(fieldName, value, schema, references, dynamicFields) {
|
|
84
|
+
const isEmpty = value === '' || value === undefined || value === null;
|
|
85
|
+
if (isEmpty && schema.isRequired) return schema.type === 'boolean' ? false : '';
|
|
86
|
+
else if (isEmpty) return undefined;
|
|
87
|
+
if (Array.isArray(value) && schema.type === 'array') {
|
|
88
|
+
return value.map((item, index)=>convertValue(`${fieldName}.${String(index)}`, item, resolve(schema.items, references), references, dynamicFields));
|
|
89
|
+
}
|
|
90
|
+
if (schema.type === 'switcher') {
|
|
91
|
+
const schema = resolve(getDynamicFieldSchema(fieldName, dynamicFields), references);
|
|
92
|
+
return convertValue(fieldName, value, schema, references, dynamicFields);
|
|
93
|
+
}
|
|
94
|
+
if (typeof value === 'object' && schema.type === 'object') {
|
|
95
|
+
const entries = Object.keys(value).map((key)=>{
|
|
96
|
+
const prop = value[key];
|
|
97
|
+
const propFieldName = `${fieldName}.${key}`;
|
|
98
|
+
if (key in schema.properties) {
|
|
99
|
+
return [
|
|
100
|
+
key,
|
|
101
|
+
convertValue(propFieldName, prop, resolve(schema.properties[key], references), references, dynamicFields)
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
if (schema.additionalProperties) {
|
|
105
|
+
const schema = resolve(getDynamicFieldSchema(propFieldName, dynamicFields), references);
|
|
106
|
+
return [
|
|
107
|
+
key,
|
|
108
|
+
convertValue(propFieldName, prop, schema, references, dynamicFields)
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
console.warn('Could not resolve field', propFieldName, dynamicFields);
|
|
112
|
+
return [
|
|
113
|
+
key,
|
|
114
|
+
prop
|
|
115
|
+
];
|
|
116
|
+
});
|
|
117
|
+
return Object.fromEntries(entries);
|
|
118
|
+
}
|
|
119
|
+
switch(schema.type){
|
|
120
|
+
case 'number':
|
|
121
|
+
return Number(value);
|
|
122
|
+
case 'boolean':
|
|
123
|
+
return value === 'null' ? undefined : value === 'true';
|
|
124
|
+
case 'file':
|
|
125
|
+
return value; // file
|
|
126
|
+
default:
|
|
127
|
+
return String(value);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function getDynamicFieldSchema(name, dynamicFields) {
|
|
131
|
+
const field = dynamicFields.get(name);
|
|
132
|
+
return field?.type === 'field' ? field.schema : {
|
|
133
|
+
type: 'null',
|
|
134
|
+
isRequired: false
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { createBodyFromValue, createBrowserFetcher };
|
|
@@ -5,13 +5,12 @@ import { forwardRef, useId, createContext, useContext, useState, useCallback, us
|
|
|
5
5
|
import { FormProvider, Controller, useFormContext, useFieldArray, useForm, useWatch } from 'react-hook-form';
|
|
6
6
|
import { Accordions, Accordion } from 'fumadocs-ui/components/accordion';
|
|
7
7
|
import { cn, buttonVariants } from 'fumadocs-ui/components/api';
|
|
8
|
-
import { C as
|
|
8
|
+
import { C as ChevronDown, a as ChevronUp, b as Check, u as useSchemaContext, T as Trash2, P as Plus, c as useApiContext, d as CircleCheck, e as CircleX, S as SchemaContext } from './client-client-DTw64k7r.js';
|
|
9
9
|
import { Slot } from '@radix-ui/react-slot';
|
|
10
10
|
import { cva } from 'class-variance-authority';
|
|
11
11
|
import { useOnChange } from 'fumadocs-core/utils/use-on-change';
|
|
12
12
|
import * as SelectPrimitive from '@radix-ui/react-select';
|
|
13
|
-
import
|
|
14
|
-
import { useShiki } from 'fumadocs-core/utils/use-shiki';
|
|
13
|
+
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock';
|
|
15
14
|
|
|
16
15
|
const Form = FormProvider;
|
|
17
16
|
const FormFieldContext = /*#__PURE__*/ createContext({
|
|
@@ -102,145 +101,6 @@ FormDescription.displayName = 'FormDescription';
|
|
|
102
101
|
};
|
|
103
102
|
}
|
|
104
103
|
|
|
105
|
-
/**
|
|
106
|
-
* Create request body from value
|
|
107
|
-
*/ function createBodyFromValue(type, value, schema, references, dynamic) {
|
|
108
|
-
const result = convertValue('body', value, schema, references, dynamic);
|
|
109
|
-
if (type === 'json') {
|
|
110
|
-
return JSON.stringify(result);
|
|
111
|
-
}
|
|
112
|
-
const formData = new FormData();
|
|
113
|
-
if (typeof result !== 'object' || !result) {
|
|
114
|
-
throw new Error(`Unsupported body type: ${typeof result}, expected: object`);
|
|
115
|
-
}
|
|
116
|
-
for (const key of Object.keys(result)){
|
|
117
|
-
const prop = result[key];
|
|
118
|
-
if (typeof prop === 'object' && prop instanceof File) {
|
|
119
|
-
formData.set(key, prop);
|
|
120
|
-
}
|
|
121
|
-
if (Array.isArray(prop) && prop.every((item)=>item instanceof File)) {
|
|
122
|
-
for (const item of prop){
|
|
123
|
-
formData.append(key, item);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (prop && !(prop instanceof File)) {
|
|
127
|
-
formData.set(key, JSON.stringify(prop));
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return formData;
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Convert a value (object or string) to the corresponding type of schema
|
|
134
|
-
*
|
|
135
|
-
* @param fieldName - field name of value
|
|
136
|
-
* @param value - the original value
|
|
137
|
-
* @param schema - the schema of field
|
|
138
|
-
* @param references - schema references
|
|
139
|
-
* @param dynamic - Dynamic references
|
|
140
|
-
*/ function convertValue(fieldName, value, schema, references, dynamic) {
|
|
141
|
-
const isEmpty = value === '' || value === undefined || value === null;
|
|
142
|
-
if (isEmpty && schema.isRequired) return schema.type === 'boolean' ? false : '';
|
|
143
|
-
else if (isEmpty) return undefined;
|
|
144
|
-
if (Array.isArray(value) && schema.type === 'array') {
|
|
145
|
-
return value.map((item, index)=>convertValue(`${fieldName}.${String(index)}`, item, resolve(schema.items, references), references, dynamic));
|
|
146
|
-
}
|
|
147
|
-
if (schema.type === 'switcher') {
|
|
148
|
-
return convertDynamicValue(fieldName, value, references, dynamic);
|
|
149
|
-
}
|
|
150
|
-
if (typeof value === 'object' && schema.type === 'object') {
|
|
151
|
-
const entries = Object.keys(value).map((key)=>{
|
|
152
|
-
const prop = value[key];
|
|
153
|
-
const propFieldName = `${fieldName}.${key}`;
|
|
154
|
-
if (key in schema.properties) {
|
|
155
|
-
return [
|
|
156
|
-
key,
|
|
157
|
-
convertValue(propFieldName, prop, resolve(schema.properties[key], references), references, dynamic)
|
|
158
|
-
];
|
|
159
|
-
}
|
|
160
|
-
if (schema.additionalProperties) {
|
|
161
|
-
return [
|
|
162
|
-
key,
|
|
163
|
-
convertDynamicValue(propFieldName, prop, references, dynamic)
|
|
164
|
-
];
|
|
165
|
-
}
|
|
166
|
-
console.warn('Could not resolve field', propFieldName, dynamic);
|
|
167
|
-
return [
|
|
168
|
-
key,
|
|
169
|
-
prop
|
|
170
|
-
];
|
|
171
|
-
});
|
|
172
|
-
return Object.fromEntries(entries);
|
|
173
|
-
}
|
|
174
|
-
switch(schema.type){
|
|
175
|
-
case 'number':
|
|
176
|
-
return Number(value);
|
|
177
|
-
case 'boolean':
|
|
178
|
-
return value === 'null' ? undefined : value === 'true';
|
|
179
|
-
case 'file':
|
|
180
|
-
return value; // file
|
|
181
|
-
default:
|
|
182
|
-
return String(value);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
function convertDynamicValue(fieldName, value, references, dynamic) {
|
|
186
|
-
const fieldDynamic = dynamic.get(fieldName);
|
|
187
|
-
return convertValue(fieldName, value, fieldDynamic?.type === 'field' ? resolve(fieldDynamic.schema, references) : {
|
|
188
|
-
type: 'null',
|
|
189
|
-
isRequired: false
|
|
190
|
-
}, references, dynamic);
|
|
191
|
-
}
|
|
192
|
-
const statusMap = {
|
|
193
|
-
400: {
|
|
194
|
-
description: 'Bad Request',
|
|
195
|
-
color: 'text-red-500',
|
|
196
|
-
icon: CircleX
|
|
197
|
-
},
|
|
198
|
-
401: {
|
|
199
|
-
description: 'Unauthorized',
|
|
200
|
-
color: 'text-red-500',
|
|
201
|
-
icon: CircleX
|
|
202
|
-
},
|
|
203
|
-
403: {
|
|
204
|
-
description: 'Forbidden',
|
|
205
|
-
color: 'text-red-500',
|
|
206
|
-
icon: CircleX
|
|
207
|
-
},
|
|
208
|
-
404: {
|
|
209
|
-
description: 'Not Found',
|
|
210
|
-
color: 'text-fd-muted-foreground',
|
|
211
|
-
icon: CircleX
|
|
212
|
-
},
|
|
213
|
-
500: {
|
|
214
|
-
description: 'Internal Server Error',
|
|
215
|
-
color: 'text-red-500',
|
|
216
|
-
icon: CircleX
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
function getStatusInfo(status) {
|
|
220
|
-
if (status in statusMap) {
|
|
221
|
-
return statusMap[status];
|
|
222
|
-
}
|
|
223
|
-
if (status >= 200 && status < 300) {
|
|
224
|
-
return {
|
|
225
|
-
description: 'Successful',
|
|
226
|
-
color: 'text-green-500',
|
|
227
|
-
icon: CircleCheck
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
if (status >= 400) {
|
|
231
|
-
return {
|
|
232
|
-
description: 'Error',
|
|
233
|
-
color: 'text-red-500',
|
|
234
|
-
icon: CircleX
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
return {
|
|
238
|
-
description: 'No Description',
|
|
239
|
-
color: 'text-fd-muted-foreground',
|
|
240
|
-
icon: CircleX
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
104
|
function getDefaultValue(item, references) {
|
|
245
105
|
if (item.type === 'object') return Object.fromEntries(Object.entries(item.properties).map(([key, prop])=>[
|
|
246
106
|
key,
|
|
@@ -816,24 +676,66 @@ function ArrayInput({ fieldName, field, ...props }) {
|
|
|
816
676
|
|
|
817
677
|
function CodeBlock({ code, lang = 'json' }) {
|
|
818
678
|
const { shikiOptions } = useApiContext();
|
|
819
|
-
|
|
820
|
-
lang,
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
pre: (props)=>/*#__PURE__*/ jsx(Base.Pre, {
|
|
824
|
-
className: "max-h-[288px]",
|
|
825
|
-
...props,
|
|
826
|
-
children: props.children
|
|
827
|
-
})
|
|
828
|
-
}
|
|
829
|
-
});
|
|
830
|
-
return /*#__PURE__*/ jsx(Base.CodeBlock, {
|
|
831
|
-
className: "my-0",
|
|
832
|
-
children: rendered
|
|
679
|
+
return /*#__PURE__*/ jsx(DynamicCodeBlock, {
|
|
680
|
+
lang: lang,
|
|
681
|
+
code: code,
|
|
682
|
+
options: shikiOptions
|
|
833
683
|
});
|
|
834
684
|
}
|
|
835
685
|
|
|
836
|
-
|
|
686
|
+
const statusMap = {
|
|
687
|
+
400: {
|
|
688
|
+
description: 'Bad Request',
|
|
689
|
+
color: 'text-red-500',
|
|
690
|
+
icon: CircleX
|
|
691
|
+
},
|
|
692
|
+
401: {
|
|
693
|
+
description: 'Unauthorized',
|
|
694
|
+
color: 'text-red-500',
|
|
695
|
+
icon: CircleX
|
|
696
|
+
},
|
|
697
|
+
403: {
|
|
698
|
+
description: 'Forbidden',
|
|
699
|
+
color: 'text-red-500',
|
|
700
|
+
icon: CircleX
|
|
701
|
+
},
|
|
702
|
+
404: {
|
|
703
|
+
description: 'Not Found',
|
|
704
|
+
color: 'text-fd-muted-foreground',
|
|
705
|
+
icon: CircleX
|
|
706
|
+
},
|
|
707
|
+
500: {
|
|
708
|
+
description: 'Internal Server Error',
|
|
709
|
+
color: 'text-red-500',
|
|
710
|
+
icon: CircleX
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
function getStatusInfo(status) {
|
|
714
|
+
if (status in statusMap) {
|
|
715
|
+
return statusMap[status];
|
|
716
|
+
}
|
|
717
|
+
if (status >= 200 && status < 300) {
|
|
718
|
+
return {
|
|
719
|
+
description: 'Successful',
|
|
720
|
+
color: 'text-green-500',
|
|
721
|
+
icon: CircleCheck
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
if (status >= 400) {
|
|
725
|
+
return {
|
|
726
|
+
description: 'Error',
|
|
727
|
+
color: 'text-red-500',
|
|
728
|
+
icon: CircleX
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
description: 'No Description',
|
|
733
|
+
color: 'text-fd-muted-foreground',
|
|
734
|
+
icon: CircleX
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function APIPlayground({ route, method = 'GET', bodyType, authorization, path = [], header = [], query = [], body, fields = {}, schemas, proxyUrl }) {
|
|
837
739
|
const { baseUrl } = useApiContext();
|
|
838
740
|
const dynamicRef = useRef(new Map());
|
|
839
741
|
const form = useForm({
|
|
@@ -846,27 +748,29 @@ function APIPlayground({ route, method = 'GET', bodyType, authorization, path =
|
|
|
846
748
|
}
|
|
847
749
|
});
|
|
848
750
|
const testQuery = useQuery(async (input)=>{
|
|
849
|
-
const
|
|
850
|
-
const
|
|
851
|
-
|
|
751
|
+
const fetcher = await import('./fetcher-BuBL-ccr.js').then((mod)=>mod.createBrowserFetcher(body, schemas));
|
|
752
|
+
const targetUrl = `${baseUrl ?? window.location.origin}${createPathnameFromInput(route, input.path, input.query)}`;
|
|
753
|
+
let url;
|
|
754
|
+
if (proxyUrl) {
|
|
755
|
+
url = new URL(proxyUrl, window.location.origin);
|
|
756
|
+
url.searchParams.append('url', targetUrl);
|
|
757
|
+
} else {
|
|
758
|
+
url = new URL(targetUrl);
|
|
759
|
+
}
|
|
760
|
+
const header = {
|
|
761
|
+
...input.header
|
|
762
|
+
};
|
|
852
763
|
if (input.authorization && authorization) {
|
|
853
|
-
|
|
764
|
+
header[authorization.name] = input.authorization;
|
|
854
765
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
method
|
|
862
|
-
headers,
|
|
863
|
-
body: bodyValue
|
|
766
|
+
return fetcher.fetch({
|
|
767
|
+
type: bodyType,
|
|
768
|
+
url: url.toString(),
|
|
769
|
+
header,
|
|
770
|
+
body: input.body,
|
|
771
|
+
dynamicFields: dynamicRef.current,
|
|
772
|
+
method
|
|
864
773
|
});
|
|
865
|
-
const data = await response.json().catch(()=>undefined);
|
|
866
|
-
return {
|
|
867
|
-
status: response.status,
|
|
868
|
-
data
|
|
869
|
-
};
|
|
870
774
|
});
|
|
871
775
|
useEffect(()=>{
|
|
872
776
|
if (!authorization) return;
|
|
@@ -902,6 +806,7 @@ function APIPlayground({ route, method = 'GET', bodyType, authorization, path =
|
|
|
902
806
|
field: info
|
|
903
807
|
}, key);
|
|
904
808
|
}
|
|
809
|
+
const isParamEmpty = path.length === 0 && query.length === 0 && header.length === 0 && body === undefined;
|
|
905
810
|
return /*#__PURE__*/ jsx(Form, {
|
|
906
811
|
...form,
|
|
907
812
|
children: /*#__PURE__*/ jsx(SchemaContext.Provider, {
|
|
@@ -932,9 +837,9 @@ function APIPlayground({ route, method = 'GET', bodyType, authorization, path =
|
|
|
932
837
|
]
|
|
933
838
|
}),
|
|
934
839
|
authorization ? renderCustomField('authorization', authorization, fields.auth) : null,
|
|
935
|
-
/*#__PURE__*/ jsxs(Accordions, {
|
|
840
|
+
!isParamEmpty ? /*#__PURE__*/ jsxs(Accordions, {
|
|
936
841
|
type: "multiple",
|
|
937
|
-
className:
|
|
842
|
+
className: "-m-3 border-0 bg-transparent text-sm",
|
|
938
843
|
children: [
|
|
939
844
|
path.length > 0 ? /*#__PURE__*/ jsx(Accordion, {
|
|
940
845
|
title: "Path",
|
|
@@ -965,7 +870,7 @@ function APIPlayground({ route, method = 'GET', bodyType, authorization, path =
|
|
|
965
870
|
}) : renderCustomField('body', body, fields.body)
|
|
966
871
|
}) : null
|
|
967
872
|
]
|
|
968
|
-
}),
|
|
873
|
+
}) : null,
|
|
969
874
|
testQuery.data ? /*#__PURE__*/ jsx(ResultDisplay, {
|
|
970
875
|
data: testQuery.data
|
|
971
876
|
}) : null
|
|
@@ -974,17 +879,17 @@ function APIPlayground({ route, method = 'GET', bodyType, authorization, path =
|
|
|
974
879
|
})
|
|
975
880
|
});
|
|
976
881
|
}
|
|
977
|
-
function
|
|
882
|
+
function createPathnameFromInput(route, path, query) {
|
|
978
883
|
let pathname = route;
|
|
979
|
-
Object.keys(path)
|
|
884
|
+
for (const key of Object.keys(path)){
|
|
980
885
|
const paramValue = path[key];
|
|
981
886
|
if (typeof paramValue === 'string' && paramValue.length > 0) pathname = pathname.replace(`{${key}}`, paramValue);
|
|
982
|
-
}
|
|
887
|
+
}
|
|
983
888
|
const searchParams = new URLSearchParams();
|
|
984
|
-
Object.keys(query)
|
|
889
|
+
for (const key of Object.keys(query)){
|
|
985
890
|
const paramValue = query[key];
|
|
986
891
|
if (typeof paramValue === 'string' && paramValue.length > 0) searchParams.append(key, paramValue);
|
|
987
|
-
}
|
|
892
|
+
}
|
|
988
893
|
return searchParams.size > 0 ? `${pathname}?${searchParams.toString()}` : pathname;
|
|
989
894
|
}
|
|
990
895
|
function RouteDisplay({ route }) {
|
|
@@ -994,7 +899,7 @@ function RouteDisplay({ route }) {
|
|
|
994
899
|
'query'
|
|
995
900
|
]
|
|
996
901
|
});
|
|
997
|
-
const pathname = useMemo(()=>
|
|
902
|
+
const pathname = useMemo(()=>createPathnameFromInput(route, path, query), [
|
|
998
903
|
route,
|
|
999
904
|
path,
|
|
1000
905
|
query
|
|
@@ -1025,7 +930,8 @@ function ResultDisplay({ data }) {
|
|
|
1025
930
|
children: data.status
|
|
1026
931
|
}),
|
|
1027
932
|
data.data ? /*#__PURE__*/ jsx(CodeBlock, {
|
|
1028
|
-
|
|
933
|
+
lang: typeof data.data === 'string' && data.data.length > 50000 ? 'text' : data.type,
|
|
934
|
+
code: typeof data.data === 'string' ? data.data : JSON.stringify(data.data, null, 2)
|
|
1029
935
|
}) : null
|
|
1030
936
|
]
|
|
1031
937
|
});
|
|
@@ -1053,4 +959,9 @@ function useQuery(fn) {
|
|
|
1053
959
|
]);
|
|
1054
960
|
}
|
|
1055
961
|
|
|
1056
|
-
|
|
962
|
+
var index = {
|
|
963
|
+
__proto__: null,
|
|
964
|
+
APIPlayground: APIPlayground
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
export { index as i, resolve as r };
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as react from 'react';
|
|
3
|
-
import { ReactNode, ComponentType, ReactElement,
|
|
3
|
+
import { ReactNode, ComponentType, ReactElement, RefObject, HTMLAttributes } from 'react';
|
|
4
4
|
import { FieldPath, ControllerRenderProps, ControllerFieldState, UseFormStateReturn } from 'react-hook-form';
|
|
5
5
|
import { OpenAPIV3_1 } from 'openapi-types';
|
|
6
6
|
import Slugger from 'github-slugger';
|
|
@@ -54,6 +54,10 @@ type Awaitable<T> = T | Promise<T>;
|
|
|
54
54
|
*/
|
|
55
55
|
type DereferenceMap = Map<unknown, string>;
|
|
56
56
|
interface RenderContext {
|
|
57
|
+
/**
|
|
58
|
+
* The url of proxy to avoid CORS issues
|
|
59
|
+
*/
|
|
60
|
+
proxyUrl?: string;
|
|
57
61
|
renderer: Renderer;
|
|
58
62
|
/**
|
|
59
63
|
* dereferenced schema
|
|
@@ -134,6 +138,7 @@ interface APIPlaygroundProps {
|
|
|
134
138
|
header?: PrimitiveRequestField[];
|
|
135
139
|
body?: RequestSchema;
|
|
136
140
|
schemas: Record<string, RequestSchema>;
|
|
141
|
+
proxyUrl?: string;
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
interface ResponsesProps {
|
|
@@ -225,7 +230,7 @@ interface CustomField<TName extends FieldPath<FormValues>, Info> {
|
|
|
225
230
|
|
|
226
231
|
interface SchemaContextType {
|
|
227
232
|
references: Record<string, RequestSchema>;
|
|
228
|
-
dynamic:
|
|
233
|
+
dynamic: RefObject<Map<string, DynamicField>>;
|
|
229
234
|
}
|
|
230
235
|
type DynamicField = {
|
|
231
236
|
type: 'object';
|
package/dist/ui/index.js
CHANGED
|
@@ -3,8 +3,8 @@ import { cn } from 'fumadocs-ui/components/api';
|
|
|
3
3
|
import { Fragment } from 'react';
|
|
4
4
|
import { Accordions, Accordion } from 'fumadocs-ui/components/accordion';
|
|
5
5
|
import { cva } from 'class-variance-authority';
|
|
6
|
-
import { f as CopyRouteButton, B as BaseUrlSelect } from './client-client-
|
|
7
|
-
export { A as APIPlayground, R as Root, u as useSchemaContext } from './client-client-
|
|
6
|
+
import { f as CopyRouteButton, B as BaseUrlSelect } from './client-client-DTw64k7r.js';
|
|
7
|
+
export { A as APIPlayground, R as Root, u as useSchemaContext } from './client-client-DTw64k7r.js';
|
|
8
8
|
|
|
9
9
|
const badgeVariants = cva('rounded-xl border px-1.5 py-1 text-xs font-medium leading-[12px]', {
|
|
10
10
|
variants: {
|
|
@@ -95,9 +95,9 @@ function APIInfo({ className, route, badgeClassname, baseUrls, method = 'GET', h
|
|
|
95
95
|
function API({ children, ...props }) {
|
|
96
96
|
return /*#__PURE__*/ jsx("div", {
|
|
97
97
|
...props,
|
|
98
|
-
className: cn('flex flex-col gap-x-6 gap-y-4
|
|
98
|
+
className: cn('flex flex-col gap-x-6 gap-y-4 xl:flex-row xl:items-start', props.className),
|
|
99
99
|
style: {
|
|
100
|
-
'--fd-api-info-top': 'calc(var(--fd-nav-height) + var(--fd-banner-height) + var(--fd-
|
|
100
|
+
'--fd-api-info-top': 'calc(var(--fd-nav-height) + var(--fd-banner-height) + var(--fd-tocnav-height, 0px))',
|
|
101
101
|
...props.style
|
|
102
102
|
},
|
|
103
103
|
children: children
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fumadocs-openapi",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.10.0",
|
|
4
4
|
"description": "Generate MDX docs for your OpenAPI spec",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"NextJs",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@fumari/json-schema-to-typescript": "^1.1.2",
|
|
35
|
-
"@radix-ui/react-select": "^2.1.
|
|
35
|
+
"@radix-ui/react-select": "^2.1.4",
|
|
36
36
|
"@radix-ui/react-slot": "^1.1.1",
|
|
37
|
-
"@scalar/openapi-parser": "0.10.
|
|
37
|
+
"@scalar/openapi-parser": "0.10.2",
|
|
38
38
|
"ajv-draft-04": "^1.0.0",
|
|
39
39
|
"class-variance-authority": "^0.7.1",
|
|
40
40
|
"fast-glob": "^3.3.1",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"remark": "^15.0.1",
|
|
47
47
|
"remark-rehype": "^11.1.1",
|
|
48
48
|
"shiki": "^1.24.2",
|
|
49
|
-
"fumadocs-
|
|
50
|
-
"fumadocs-
|
|
49
|
+
"fumadocs-core": "14.6.2",
|
|
50
|
+
"fumadocs-ui": "14.6.2"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/js-yaml": "^4.0.9",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@types/react": "^19.0.1",
|
|
57
57
|
"bunchee": "^6.0.3",
|
|
58
58
|
"lucide-react": "^0.468.0",
|
|
59
|
-
"next": "15.1.
|
|
59
|
+
"next": "15.1.1",
|
|
60
60
|
"openapi-types": "^12.1.3",
|
|
61
61
|
"eslint-config-custom": "0.0.0",
|
|
62
62
|
"tsconfig": "0.0.0"
|