fumadocs-openapi 5.8.2 → 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 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/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { resolve, join, parse, dirname } from 'node:path';
2
2
  import { dump } from 'js-yaml';
3
3
  import Slugger from 'github-slugger';
4
- import Parser from '@apidevtools/json-schema-ref-parser';
5
- import { upgrade } from '@scalar/openapi-parser';
4
+ import { load, upgrade, dereference as dereference$1 } from '@scalar/openapi-parser';
5
+ import { fetchUrls } from '@scalar/openapi-parser/plugins/fetch-urls';
6
+ import { readFiles } from '@scalar/openapi-parser/plugins/read-files';
6
7
  import { mkdir, writeFile } from 'node:fs/promises';
7
8
  import fg from 'fast-glob';
8
9
 
@@ -141,17 +142,18 @@ const cache = new Map();
141
142
  */ async function processDocument(document, disableCache = false) {
142
143
  const cached = !disableCache && typeof document === 'string' ? cache.get(document) : null;
143
144
  if (cached) return cached;
144
- let bundled = await Parser.bundle(document, {
145
- mutateInputSchema: false
146
- });
147
- bundled = upgrade(bundled).specification;
148
145
  const dereferenceMap = new Map();
149
- const dereferenced = await Parser.dereference(bundled, {
150
- mutateInputSchema: true,
151
- dereference: {
152
- onDereference ($ref, schema) {
153
- dereferenceMap.set(schema, $ref);
154
- }
146
+ const loaded = await load(document, {
147
+ plugins: [
148
+ readFiles(),
149
+ fetchUrls()
150
+ ]
151
+ });
152
+ // upgrade
153
+ loaded.specification = upgrade(loaded.specification).specification;
154
+ const { schema: dereferenced } = await dereference$1(loaded.filesystem, {
155
+ onDereference ({ ref, schema }) {
156
+ dereferenceMap.set(schema, ref);
155
157
  }
156
158
  });
157
159
  const processed = {
@@ -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
- interface ApiPageProps extends Pick<RenderContext, 'generateCodeSamples' | 'generateTypeScriptSchema' | 'shikiOptions'> {
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
 
@@ -14,8 +14,9 @@ import { Accordions, Accordion } from 'fumadocs-ui/components/accordion';
14
14
  import * as Base from 'fumadocs-ui/components/codeblock';
15
15
  import { highlight } from 'fumadocs-core/server';
16
16
  import { Root, API, APIInfo, APIExample as APIExample$1, Property, ObjectCollapsible, APIPlayground } from '../ui/index.js';
17
- import Parser from '@apidevtools/json-schema-ref-parser';
18
- import { upgrade } from '@scalar/openapi-parser';
17
+ import { load, upgrade, dereference } from '@scalar/openapi-parser';
18
+ import { fetchUrls } from '@scalar/openapi-parser/plugins/fetch-urls';
19
+ import { readFiles } from '@scalar/openapi-parser/plugins/read-files';
19
20
  import { cva } from 'class-variance-authority';
20
21
 
21
22
  function getPreferredType(body) {
@@ -301,13 +302,14 @@ function Playground({ path, method, ctx }) {
301
302
  const mediaType = bodyContent ? getPreferredType(bodyContent) : undefined;
302
303
  const context = {
303
304
  allowFile: mediaType === 'multipart/form-data',
304
- schema: {},
305
+ references: {},
305
306
  nextId () {
306
307
  return String(currentId++);
307
308
  },
308
309
  registered: new WeakMap(),
309
310
  render: ctx
310
311
  };
312
+ const bodySchema = bodyContent && mediaType && bodyContent[mediaType].schema ? toSchema(bodyContent[mediaType].schema, true, context) : undefined;
311
313
  const props = {
312
314
  authorization: getAuthorizationField(method, ctx),
313
315
  method: method.method,
@@ -316,8 +318,9 @@ function Playground({ path, method, ctx }) {
316
318
  path: method.parameters?.filter((v)=>v.in === 'path').map((v)=>parameterToField(v, context)),
317
319
  query: method.parameters?.filter((v)=>v.in === 'query').map((v)=>parameterToField(v, context)),
318
320
  header: method.parameters?.filter((v)=>v.in === 'header').map((v)=>parameterToField(v, context)),
319
- body: bodyContent && mediaType && bodyContent[mediaType].schema ? toSchema(bodyContent[mediaType].schema, true, context) : undefined,
320
- schemas: context.schema
321
+ body: bodySchema,
322
+ schemas: context.references,
323
+ proxyUrl: ctx.proxyUrl
321
324
  };
322
325
  return /*#__PURE__*/ jsx(ctx.renderer.APIPlayground, {
323
326
  ...props
@@ -343,7 +346,7 @@ function getIdFromSchema(schema, required, ctx) {
343
346
  if (registered === undefined) {
344
347
  const id = ctx.nextId();
345
348
  ctx.registered.set(schema, id);
346
- ctx.schema[id] = toSchema(schema, required, ctx);
349
+ ctx.references[id] = toSchema(schema, required, ctx);
347
350
  return id;
348
351
  }
349
352
  return registered;
@@ -384,7 +387,7 @@ function toSchema(schema, required, ctx) {
384
387
  const additional = schema.additionalProperties;
385
388
  let additionalProperties;
386
389
  if (additional && typeof additional === 'object') {
387
- if (!additional.type && !additional.anyOf && !additional.allOf && !additional.oneOf) {
390
+ if ((!additional.type || additional.type.length === 0) && !additional.anyOf && !additional.allOf && !additional.oneOf) {
388
391
  additionalProperties = true;
389
392
  } else {
390
393
  additionalProperties = getIdFromSchema(additional, false, ctx);
@@ -436,12 +439,11 @@ function toSchema(schema, required, ctx) {
436
439
  items: 'items' in schema && schema.items ? toSchema(schema.items, false, ctx) : toSchema({}, required, ctx),
437
440
  isRequired: required
438
441
  };
439
- } else if (type !== 'null' && type !== 'object') {
440
- items[type] = {
441
- type: type === 'integer' ? 'number' : type,
442
- isRequired: required,
443
- defaultValue: schema.example ?? schema.default ?? ''
444
- };
442
+ } else {
443
+ items[type] = toSchema({
444
+ ...schema,
445
+ type
446
+ }, true, ctx);
445
447
  }
446
448
  }
447
449
  return {
@@ -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,
@@ -1193,17 +1195,18 @@ const cache = new Map();
1193
1195
  */ async function processDocument(document, disableCache = false) {
1194
1196
  const cached = !disableCache && typeof document === 'string' ? cache.get(document) : null;
1195
1197
  if (cached) return cached;
1196
- let bundled = await Parser.bundle(document, {
1197
- mutateInputSchema: false
1198
- });
1199
- bundled = upgrade(bundled).specification;
1200
1198
  const dereferenceMap = new Map();
1201
- const dereferenced = await Parser.dereference(bundled, {
1202
- mutateInputSchema: true,
1203
- dereference: {
1204
- onDereference ($ref, schema) {
1205
- dereferenceMap.set(schema, $ref);
1206
- }
1199
+ const loaded = await load(document, {
1200
+ plugins: [
1201
+ readFiles(),
1202
+ fetchUrls()
1203
+ ]
1204
+ });
1205
+ // upgrade
1206
+ loaded.specification = upgrade(loaded.specification).specification;
1207
+ const { schema: dereferenced } = await dereference(loaded.filesystem, {
1208
+ onDereference ({ ref, schema }) {
1209
+ dereferenceMap.set(schema, ref);
1207
1210
  }
1208
1211
  });
1209
1212
  const processed = {
@@ -1261,6 +1264,7 @@ async function getContext({ document, dereferenceMap }, options = {}) {
1261
1264
  return {
1262
1265
  document: document,
1263
1266
  dereferenceMap,
1267
+ proxyUrl: options.proxyUrl,
1264
1268
  renderer: {
1265
1269
  ...createRenders(options.shikiOptions),
1266
1270
  ...options.renderer
@@ -1274,8 +1278,69 @@ async function getContext({ document, dereferenceMap }, options = {}) {
1274
1278
  };
1275
1279
  }
1276
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
+
1277
1341
  function createOpenAPI(options = {}) {
1278
1342
  return {
1343
+ createProxy,
1279
1344
  APIPage (props) {
1280
1345
  return /*#__PURE__*/ jsx(APIPage, {
1281
1346
  ...options,
@@ -253,7 +253,7 @@ function useSchemaContext() {
253
253
  return ctx;
254
254
  }
255
255
 
256
- const APIPlayground = dynamic(()=>import('./playground-client-JckpOPq4.js').then((mod)=>mod.APIPlayground));
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, CircleCheck as C, Plus as P, Root as R, SchemaContext as S, Trash2 as T, CircleX as a, ChevronDown as b, ChevronUp as c, Check as d, useApiContext as e, CopyRouteButton as f, useSchemaContext as u };
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 CircleCheck, a as CircleX, b as ChevronDown, c as ChevronUp, d as Check, u as useSchemaContext, T as Trash2, P as Plus, e as useApiContext, S as SchemaContext } from './client-client-D5WUNeFS.js';
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 * as Base from 'fumadocs-ui/components/codeblock';
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,
@@ -248,7 +108,11 @@ function getDefaultValue(item, references) {
248
108
  ]));
249
109
  if (item.type === 'array') return [];
250
110
  if (item.type === 'null') return null;
251
- if (item.type === 'switcher') return getDefaultValue(resolve(Object.values(item.items)[0], references), references);
111
+ if (item.type === 'switcher') {
112
+ const first = Object.values(item.items).at(0);
113
+ if (!first) return '';
114
+ return getDefaultValue(resolve(first, references), references);
115
+ }
252
116
  if (item.type === 'file') return undefined;
253
117
  return String(item.defaultValue);
254
118
  }
@@ -812,24 +676,66 @@ function ArrayInput({ fieldName, field, ...props }) {
812
676
 
813
677
  function CodeBlock({ code, lang = 'json' }) {
814
678
  const { shikiOptions } = useApiContext();
815
- const rendered = useShiki(code, {
816
- lang,
817
- ...shikiOptions,
818
- components: {
819
- pre: (props)=>/*#__PURE__*/ jsx(Base.Pre, {
820
- className: "max-h-[288px]",
821
- ...props,
822
- children: props.children
823
- })
824
- }
825
- });
826
- return /*#__PURE__*/ jsx(Base.CodeBlock, {
827
- className: "my-0",
828
- children: rendered
679
+ return /*#__PURE__*/ jsx(DynamicCodeBlock, {
680
+ lang: lang,
681
+ code: code,
682
+ options: shikiOptions
829
683
  });
830
684
  }
831
685
 
832
- function APIPlayground({ route, method = 'GET', bodyType, authorization, path = [], header = [], query = [], body, fields = {}, schemas }) {
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 }) {
833
739
  const { baseUrl } = useApiContext();
834
740
  const dynamicRef = useRef(new Map());
835
741
  const form = useForm({
@@ -842,27 +748,29 @@ function APIPlayground({ route, method = 'GET', bodyType, authorization, path =
842
748
  }
843
749
  });
844
750
  const testQuery = useQuery(async (input)=>{
845
- const url = new URL(`${baseUrl ?? window.location.origin}${createUrlFromInput(route, input.path, input.query)}`);
846
- const headers = new Headers();
847
- if (bodyType !== 'form-data') headers.append('Content-Type', 'application/json');
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
+ };
848
763
  if (input.authorization && authorization) {
849
- headers.append(authorization.name, input.authorization);
764
+ header[authorization.name] = input.authorization;
850
765
  }
851
- Object.keys(input.header).forEach((key)=>{
852
- const paramValue = input.header[key];
853
- if (typeof paramValue === 'string' && paramValue.length > 0) headers.append(key, paramValue);
766
+ return fetcher.fetch({
767
+ type: bodyType,
768
+ url: url.toString(),
769
+ header,
770
+ body: input.body,
771
+ dynamicFields: dynamicRef.current,
772
+ method
854
773
  });
855
- const bodyValue = body && input.body && Object.keys(input.body).length > 0 ? createBodyFromValue(bodyType, input.body, body, schemas, dynamicRef.current) : undefined;
856
- const response = await fetch(url, {
857
- method,
858
- headers,
859
- body: bodyValue
860
- });
861
- const data = await response.json().catch(()=>undefined);
862
- return {
863
- status: response.status,
864
- data
865
- };
866
774
  });
867
775
  useEffect(()=>{
868
776
  if (!authorization) return;
@@ -898,6 +806,7 @@ function APIPlayground({ route, method = 'GET', bodyType, authorization, path =
898
806
  field: info
899
807
  }, key);
900
808
  }
809
+ const isParamEmpty = path.length === 0 && query.length === 0 && header.length === 0 && body === undefined;
901
810
  return /*#__PURE__*/ jsx(Form, {
902
811
  ...form,
903
812
  children: /*#__PURE__*/ jsx(SchemaContext.Provider, {
@@ -928,9 +837,9 @@ function APIPlayground({ route, method = 'GET', bodyType, authorization, path =
928
837
  ]
929
838
  }),
930
839
  authorization ? renderCustomField('authorization', authorization, fields.auth) : null,
931
- /*#__PURE__*/ jsxs(Accordions, {
840
+ !isParamEmpty ? /*#__PURE__*/ jsxs(Accordions, {
932
841
  type: "multiple",
933
- className: cn('-m-3 border-0 bg-transparent text-sm', path.length === 0 && query.length === 0 && header.length === 0 && !body && 'hidden'),
842
+ className: "-m-3 border-0 bg-transparent text-sm",
934
843
  children: [
935
844
  path.length > 0 ? /*#__PURE__*/ jsx(Accordion, {
936
845
  title: "Path",
@@ -961,7 +870,7 @@ function APIPlayground({ route, method = 'GET', bodyType, authorization, path =
961
870
  }) : renderCustomField('body', body, fields.body)
962
871
  }) : null
963
872
  ]
964
- }),
873
+ }) : null,
965
874
  testQuery.data ? /*#__PURE__*/ jsx(ResultDisplay, {
966
875
  data: testQuery.data
967
876
  }) : null
@@ -970,17 +879,17 @@ function APIPlayground({ route, method = 'GET', bodyType, authorization, path =
970
879
  })
971
880
  });
972
881
  }
973
- function createUrlFromInput(route, path, query) {
882
+ function createPathnameFromInput(route, path, query) {
974
883
  let pathname = route;
975
- Object.keys(path).forEach((key)=>{
884
+ for (const key of Object.keys(path)){
976
885
  const paramValue = path[key];
977
886
  if (typeof paramValue === 'string' && paramValue.length > 0) pathname = pathname.replace(`{${key}}`, paramValue);
978
- });
887
+ }
979
888
  const searchParams = new URLSearchParams();
980
- Object.keys(query).forEach((key)=>{
889
+ for (const key of Object.keys(query)){
981
890
  const paramValue = query[key];
982
891
  if (typeof paramValue === 'string' && paramValue.length > 0) searchParams.append(key, paramValue);
983
- });
892
+ }
984
893
  return searchParams.size > 0 ? `${pathname}?${searchParams.toString()}` : pathname;
985
894
  }
986
895
  function RouteDisplay({ route }) {
@@ -990,7 +899,7 @@ function RouteDisplay({ route }) {
990
899
  'query'
991
900
  ]
992
901
  });
993
- const pathname = useMemo(()=>createUrlFromInput(route, path, query), [
902
+ const pathname = useMemo(()=>createPathnameFromInput(route, path, query), [
994
903
  route,
995
904
  path,
996
905
  query
@@ -1021,7 +930,8 @@ function ResultDisplay({ data }) {
1021
930
  children: data.status
1022
931
  }),
1023
932
  data.data ? /*#__PURE__*/ jsx(CodeBlock, {
1024
- code: JSON.stringify(data.data, null, 2)
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)
1025
935
  }) : null
1026
936
  ]
1027
937
  });
@@ -1049,4 +959,9 @@ function useQuery(fn) {
1049
959
  ]);
1050
960
  }
1051
961
 
1052
- export { APIPlayground };
962
+ var index = {
963
+ __proto__: null,
964
+ APIPlayground: APIPlayground
965
+ };
966
+
967
+ export { index as i, resolve as r };
@@ -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, MutableRefObject, HTMLAttributes } from 'react';
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: MutableRefObject<Map<string, DynamicField>>;
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-D5WUNeFS.js';
7
- export { A as APIPlayground, R as Root, u as useSchemaContext } from './client-client-D5WUNeFS.js';
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 max-xl:[--fd-toc-height:46px] max-md:[--fd-toc-height:36px] xl:flex-row xl:items-start', props.className),
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-toc-height, 0.5rem))',
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.8.2",
3
+ "version": "5.10.0",
4
4
  "description": "Generate MDX docs for your OpenAPI spec",
5
5
  "keywords": [
6
6
  "NextJs",
@@ -31,32 +31,32 @@
31
31
  "dist"
32
32
  ],
33
33
  "dependencies": {
34
- "@apidevtools/json-schema-ref-parser": "^11.7.3",
35
34
  "@fumari/json-schema-to-typescript": "^1.1.2",
36
- "@radix-ui/react-select": "^2.1.2",
37
- "@radix-ui/react-slot": "^1.1.0",
38
- "@scalar/openapi-parser": "^0.8.10",
35
+ "@radix-ui/react-select": "^2.1.4",
36
+ "@radix-ui/react-slot": "^1.1.1",
37
+ "@scalar/openapi-parser": "0.10.2",
38
+ "ajv-draft-04": "^1.0.0",
39
39
  "class-variance-authority": "^0.7.1",
40
40
  "fast-glob": "^3.3.1",
41
41
  "github-slugger": "^2.0.0",
42
42
  "hast-util-to-jsx-runtime": "^2.3.2",
43
43
  "js-yaml": "^4.1.0",
44
44
  "openapi-sampler": "^1.6.1",
45
- "react-hook-form": "^7.54.0",
45
+ "react-hook-form": "^7.54.1",
46
46
  "remark": "^15.0.1",
47
47
  "remark-rehype": "^11.1.1",
48
48
  "shiki": "^1.24.2",
49
- "fumadocs-core": "14.6.0",
50
- "fumadocs-ui": "14.6.0"
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",
54
- "@types/node": "22.10.1",
54
+ "@types/node": "22.10.2",
55
55
  "@types/openapi-sampler": "^1.0.3",
56
56
  "@types/react": "^19.0.1",
57
57
  "bunchee": "^6.0.3",
58
58
  "lucide-react": "^0.468.0",
59
- "next": "15.0.4",
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"