fumadocs-openapi 5.4.14 → 5.5.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
@@ -1,6 +1,7 @@
1
1
  import { OpenAPIV3 } from 'openapi-types';
2
2
  import Slugger from 'github-slugger';
3
3
  import { ComponentType, ReactNode } from 'react';
4
+ import { CodeToHastOptionsCommon, CodeOptionsThemes, BuiltinTheme } from 'shiki';
4
5
 
5
6
  interface BaseRequestField {
6
7
  name: string;
@@ -189,6 +190,7 @@ interface RenderContext {
189
190
  * Generate code samples for endpoint.
190
191
  */
191
192
  generateCodeSamples?: (endpoint: EndpointSample) => Awaitable<CodeSample[]>;
193
+ shikiOptions?: Omit<CodeToHastOptionsCommon, 'lang'> & CodeOptionsThemes<BuiltinTheme>;
192
194
  }
193
195
 
194
196
  type DocumentContext = {
package/dist/index.js CHANGED
@@ -228,6 +228,7 @@ async function generateTags(pathOrDocument, options = {}) {
228
228
  });
229
229
  }
230
230
  }
231
+ const displayName = info && 'x-displayName' in info && typeof info['x-displayName'] === 'string' ? info['x-displayName'] : idToTitle(tag);
231
232
  return {
232
233
  tag,
233
234
  content: generateDocument({
@@ -238,7 +239,7 @@ async function generateTags(pathOrDocument, options = {}) {
238
239
  hasHead: true
239
240
  },
240
241
  dereferenced: document,
241
- title: idToTitle(tag),
242
+ title: displayName,
242
243
  description: info?.description,
243
244
  context: {
244
245
  type: 'tag',
@@ -1,6 +1,7 @@
1
1
  import { OpenAPIV3 } from 'openapi-types';
2
2
  import { ComponentType, ReactNode, FC } from 'react';
3
3
  import Slugger from 'github-slugger';
4
+ import { CodeToHastOptionsCommon, CodeOptionsThemes, BuiltinTheme } from 'shiki';
4
5
  import { BuildPageTreeOptions } from 'fumadocs-core/source';
5
6
 
6
7
  interface BaseRequestField {
@@ -180,9 +181,10 @@ interface RenderContext {
180
181
  * Generate code samples for endpoint.
181
182
  */
182
183
  generateCodeSamples?: (endpoint: EndpointSample) => Awaitable<CodeSample[]>;
184
+ shikiOptions?: Omit<CodeToHastOptionsCommon, 'lang'> & CodeOptionsThemes<BuiltinTheme>;
183
185
  }
184
186
 
185
- interface ApiPageProps extends Pick<RenderContext, 'generateCodeSamples' | 'generateTypeScriptSchema'> {
187
+ interface ApiPageProps extends Pick<RenderContext, 'generateCodeSamples' | 'generateTypeScriptSchema' | 'shikiOptions'> {
186
188
  document: string | OpenAPIV3.Document;
187
189
  /**
188
190
  * An array of operations
@@ -190,6 +192,7 @@ interface ApiPageProps extends Pick<RenderContext, 'generateCodeSamples' | 'gene
190
192
  operations: Operation[];
191
193
  hasHead: boolean;
192
194
  renderer?: Partial<Renderer>;
195
+ disableCache?: boolean;
193
196
  }
194
197
  interface Operation {
195
198
  path: string;
@@ -79,7 +79,7 @@ function generateSample(path, method, { baseUrl, document }) {
79
79
  for (const security of getSecurities(requirements[0], document)){
80
80
  const prefix = getSecurityPrefix(security);
81
81
  params.push({
82
- name: 'Authorization',
82
+ name: security.type === 'apiKey' ? security.name : 'Authorization',
83
83
  schema: {
84
84
  type: 'string'
85
85
  },
@@ -274,7 +274,11 @@ print(response.text)`;
274
274
 
275
275
  async function getTypescriptSchema(endpoint, code) {
276
276
  if (code in endpoint.responses) {
277
- return compile(endpoint.responses[code].schema, 'Response', {
277
+ return compile(// re-running on the same schema results in error
278
+ // because it uses `defineProperty` to define internal references
279
+ // we clone the schema to fix this problem
280
+ structuredClone(endpoint.responses[code].schema), 'Response', {
281
+ $refOptions: false,
278
282
  bannerComment: '',
279
283
  additionalProperties: false,
280
284
  format: true,
@@ -435,9 +439,28 @@ function idToTitle(id) {
435
439
  return result.join('');
436
440
  }
437
441
 
442
+ const sharedTransformers = [
443
+ {
444
+ name: 'fumadocs:pre-process',
445
+ line (hast) {
446
+ if (hast.children.length === 0) {
447
+ // Keep the empty lines when using grid layout
448
+ hast.children.push({
449
+ type: 'text',
450
+ value: ' '
451
+ });
452
+ }
453
+ }
454
+ }
455
+ ];
456
+
438
457
  const processor = remark().use(remarkGfm).use(remarkImage, {
439
458
  useImport: false
440
- }).use(remarkRehype).use(rehypeCode);
459
+ }).use(remarkRehype).use(rehypeCode, {
460
+ langs: [],
461
+ lazy: true,
462
+ transformers: sharedTransformers
463
+ });
441
464
  async function Markdown({ text }) {
442
465
  const nodes = processor.parse({
443
466
  value: text
@@ -992,22 +1015,7 @@ async function ResponseTabs({ endpoint, operation, ctx: { renderer, generateType
992
1015
  };
993
1016
  }
994
1017
 
995
- const sharedTransformers = [
996
- {
997
- name: 'fumadocs:pre-process',
998
- line (hast) {
999
- if (hast.children.length === 0) {
1000
- // Keep the empty lines when using grid layout
1001
- hast.children.push({
1002
- type: 'text',
1003
- value: ' '
1004
- });
1005
- }
1006
- }
1007
- }
1008
- ];
1009
-
1010
- async function CodeBlock({ code, lang, ...options }) {
1018
+ async function CodeBlock({ code, lang, options, ...rest }) {
1011
1019
  const html = await codeToHast(code, {
1012
1020
  lang,
1013
1021
  defaultColor: false,
@@ -1015,22 +1023,20 @@ async function CodeBlock({ code, lang, ...options }) {
1015
1023
  light: 'github-light',
1016
1024
  dark: 'github-dark'
1017
1025
  },
1018
- transformers: sharedTransformers
1026
+ transformers: sharedTransformers,
1027
+ ...options
1019
1028
  });
1020
1029
  const codeblock = toJsxRuntime(html, {
1021
1030
  development: false,
1022
- // @ts-expect-error -- untyped
1023
- jsx,
1024
- // @ts-expect-error -- untyped
1025
- jsxs,
1031
+ jsx: jsx,
1032
+ jsxs: jsxs,
1033
+ Fragment: Fragment$1,
1026
1034
  components: {
1027
- // eslint-disable-next-line react/no-unstable-nested-components -- server component
1028
1035
  pre: (props)=>/*#__PURE__*/ jsx(Base.Pre, {
1029
1036
  ...props,
1030
- ...options
1037
+ ...rest
1031
1038
  })
1032
- },
1033
- Fragment: Fragment$1
1039
+ }
1034
1040
  });
1035
1041
  return /*#__PURE__*/ jsx(Base.CodeBlock, {
1036
1042
  className: "my-0",
@@ -1038,47 +1044,55 @@ async function CodeBlock({ code, lang, ...options }) {
1038
1044
  });
1039
1045
  }
1040
1046
 
1041
- const defaultRenderer = {
1042
- Root,
1043
- API,
1044
- APIInfo,
1045
- APIExample: APIExample$1,
1046
- Responses: Tabs,
1047
- Response: Tab,
1048
- ResponseTypes: (props)=>/*#__PURE__*/ jsx(Accordions, {
1049
- type: "single",
1050
- className: "!-m-4 border-none pt-2",
1051
- defaultValue: "Response",
1052
- children: props.children
1053
- }),
1054
- ResponseType: (props)=>/*#__PURE__*/ jsx(Accordion, {
1055
- title: props.label,
1056
- children: /*#__PURE__*/ jsx(CodeBlock, {
1057
- code: props.code,
1058
- lang: props.lang
1059
- })
1060
- }),
1061
- Property,
1062
- ObjectCollapsible,
1063
- Requests: (props)=>/*#__PURE__*/ jsx(Tabs, {
1064
- groupId: "fumadocs_openapi_requests",
1065
- ...props
1066
- }),
1067
- Request: (props)=>/*#__PURE__*/ jsx(Tab, {
1068
- value: props.name,
1069
- children: /*#__PURE__*/ jsx(CodeBlock, {
1070
- lang: props.language,
1071
- code: props.code
1072
- })
1073
- }),
1074
- APIPlayground
1075
- };
1047
+ function createRenders(shikiOptions) {
1048
+ return {
1049
+ Root: (props)=>/*#__PURE__*/ jsx(Root, {
1050
+ shikiOptions: shikiOptions,
1051
+ ...props,
1052
+ children: props.children
1053
+ }),
1054
+ API,
1055
+ APIInfo,
1056
+ APIExample: APIExample$1,
1057
+ Responses: Tabs,
1058
+ Response: Tab,
1059
+ ResponseTypes: (props)=>/*#__PURE__*/ jsx(Accordions, {
1060
+ type: "single",
1061
+ className: "!-m-4 border-none pt-2",
1062
+ defaultValue: "Response",
1063
+ children: props.children
1064
+ }),
1065
+ ResponseType: (props)=>/*#__PURE__*/ jsx(Accordion, {
1066
+ title: props.label,
1067
+ children: /*#__PURE__*/ jsx(CodeBlock, {
1068
+ code: props.code,
1069
+ lang: props.lang,
1070
+ options: shikiOptions
1071
+ })
1072
+ }),
1073
+ Property,
1074
+ ObjectCollapsible,
1075
+ Requests: (props)=>/*#__PURE__*/ jsx(Tabs, {
1076
+ groupId: "fumadocs_openapi_requests",
1077
+ ...props
1078
+ }),
1079
+ Request: (props)=>/*#__PURE__*/ jsx(Tab, {
1080
+ value: props.name,
1081
+ children: /*#__PURE__*/ jsx(CodeBlock, {
1082
+ lang: props.language,
1083
+ code: props.code,
1084
+ options: shikiOptions
1085
+ })
1086
+ }),
1087
+ APIPlayground
1088
+ };
1089
+ }
1076
1090
 
1077
1091
  const cache = new Map();
1078
1092
  async function APIPage(props) {
1079
1093
  const { operations, hasHead = true } = props;
1080
1094
  let document;
1081
- if (typeof props.document === 'string') {
1095
+ if (typeof props.document === 'string' && !props.disableCache) {
1082
1096
  const cached = cache.get(props.document);
1083
1097
  document = cached ?? await Parser.dereference(props.document);
1084
1098
  cache.set(props.document, document);
@@ -1107,9 +1121,10 @@ function getContext(document, options) {
1107
1121
  return {
1108
1122
  document,
1109
1123
  renderer: {
1110
- ...defaultRenderer,
1124
+ ...createRenders(options.shikiOptions),
1111
1125
  ...options.renderer
1112
1126
  },
1127
+ shikiOptions: options.shikiOptions,
1113
1128
  generateTypeScriptSchema: options.generateTypeScriptSchema,
1114
1129
  generateCodeSamples: options.generateCodeSamples,
1115
1130
  baseUrl: document.servers?.[0].url ?? 'https://example.com',
@@ -0,0 +1,317 @@
1
+ 'use client';
2
+ import { forwardRef, createElement, useContext, createContext, useState, useEffect, useMemo } from 'react';
3
+ import { jsx } from 'react/jsx-runtime';
4
+ import { cn, useCopyButton, buttonVariants } from 'fumadocs-ui/components/api';
5
+ import dynamic from 'next/dynamic';
6
+ import { useOnChange } from 'fumadocs-core/utils/use-on-change';
7
+
8
+ /**
9
+ * @license lucide-react v0.453.0 - ISC
10
+ *
11
+ * This source code is licensed under the ISC license.
12
+ * See the LICENSE file in the root directory of this source tree.
13
+ */ const toKebabCase = (string)=>string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
14
+ const mergeClasses = (...classes)=>classes.filter((className, index, array)=>{
15
+ return Boolean(className) && array.indexOf(className) === index;
16
+ }).join(" ");
17
+
18
+ /**
19
+ * @license lucide-react v0.453.0 - ISC
20
+ *
21
+ * This source code is licensed under the ISC license.
22
+ * See the LICENSE file in the root directory of this source tree.
23
+ */ var defaultAttributes = {
24
+ xmlns: "http://www.w3.org/2000/svg",
25
+ width: 24,
26
+ height: 24,
27
+ viewBox: "0 0 24 24",
28
+ fill: "none",
29
+ stroke: "currentColor",
30
+ strokeWidth: 2,
31
+ strokeLinecap: "round",
32
+ strokeLinejoin: "round"
33
+ };
34
+
35
+ const Icon = /*#__PURE__*/ forwardRef(({ color = "currentColor", size = 24, strokeWidth = 2, absoluteStrokeWidth, className = "", children, iconNode, ...rest }, ref)=>{
36
+ return /*#__PURE__*/ createElement("svg", {
37
+ ref,
38
+ ...defaultAttributes,
39
+ width: size,
40
+ height: size,
41
+ stroke: color,
42
+ strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth,
43
+ className: mergeClasses("lucide", className),
44
+ ...rest
45
+ }, [
46
+ ...iconNode.map(([tag, attrs])=>/*#__PURE__*/ createElement(tag, attrs)),
47
+ ...Array.isArray(children) ? children : [
48
+ children
49
+ ]
50
+ ]);
51
+ });
52
+
53
+ const createLucideIcon = (iconName, iconNode)=>{
54
+ const Component = /*#__PURE__*/ forwardRef(({ className, ...props }, ref)=>/*#__PURE__*/ createElement(Icon, {
55
+ ref,
56
+ iconNode,
57
+ className: mergeClasses(`lucide-${toKebabCase(iconName)}`, className),
58
+ ...props
59
+ }));
60
+ Component.displayName = `${iconName}`;
61
+ return Component;
62
+ };
63
+
64
+ const Check = createLucideIcon("Check", [
65
+ [
66
+ "path",
67
+ {
68
+ d: "M20 6 9 17l-5-5",
69
+ key: "1gmf2c"
70
+ }
71
+ ]
72
+ ]);
73
+
74
+ const ChevronDown = createLucideIcon("ChevronDown", [
75
+ [
76
+ "path",
77
+ {
78
+ d: "m6 9 6 6 6-6",
79
+ key: "qrunsl"
80
+ }
81
+ ]
82
+ ]);
83
+
84
+ const ChevronUp = createLucideIcon("ChevronUp", [
85
+ [
86
+ "path",
87
+ {
88
+ d: "m18 15-6-6-6 6",
89
+ key: "153udz"
90
+ }
91
+ ]
92
+ ]);
93
+
94
+ const CircleCheck = createLucideIcon("CircleCheck", [
95
+ [
96
+ "circle",
97
+ {
98
+ cx: "12",
99
+ cy: "12",
100
+ r: "10",
101
+ key: "1mglay"
102
+ }
103
+ ],
104
+ [
105
+ "path",
106
+ {
107
+ d: "m9 12 2 2 4-4",
108
+ key: "dzmm74"
109
+ }
110
+ ]
111
+ ]);
112
+
113
+ const CircleX = createLucideIcon("CircleX", [
114
+ [
115
+ "circle",
116
+ {
117
+ cx: "12",
118
+ cy: "12",
119
+ r: "10",
120
+ key: "1mglay"
121
+ }
122
+ ],
123
+ [
124
+ "path",
125
+ {
126
+ d: "m15 9-6 6",
127
+ key: "1uzhvr"
128
+ }
129
+ ],
130
+ [
131
+ "path",
132
+ {
133
+ d: "m9 9 6 6",
134
+ key: "z0biqf"
135
+ }
136
+ ]
137
+ ]);
138
+
139
+ const Copy = createLucideIcon("Copy", [
140
+ [
141
+ "rect",
142
+ {
143
+ width: "14",
144
+ height: "14",
145
+ x: "8",
146
+ y: "8",
147
+ rx: "2",
148
+ ry: "2",
149
+ key: "17jyea"
150
+ }
151
+ ],
152
+ [
153
+ "path",
154
+ {
155
+ d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",
156
+ key: "zix9uf"
157
+ }
158
+ ]
159
+ ]);
160
+
161
+ const Plus = createLucideIcon("Plus", [
162
+ [
163
+ "path",
164
+ {
165
+ d: "M5 12h14",
166
+ key: "1ays0h"
167
+ }
168
+ ],
169
+ [
170
+ "path",
171
+ {
172
+ d: "M12 5v14",
173
+ key: "s699le"
174
+ }
175
+ ]
176
+ ]);
177
+
178
+ const Trash2 = createLucideIcon("Trash2", [
179
+ [
180
+ "path",
181
+ {
182
+ d: "M3 6h18",
183
+ key: "d0wm0j"
184
+ }
185
+ ],
186
+ [
187
+ "path",
188
+ {
189
+ d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",
190
+ key: "4alrt4"
191
+ }
192
+ ],
193
+ [
194
+ "path",
195
+ {
196
+ d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",
197
+ key: "v07s0e"
198
+ }
199
+ ],
200
+ [
201
+ "line",
202
+ {
203
+ x1: "10",
204
+ x2: "10",
205
+ y1: "11",
206
+ y2: "17",
207
+ key: "1uufr5"
208
+ }
209
+ ],
210
+ [
211
+ "line",
212
+ {
213
+ x1: "14",
214
+ x2: "14",
215
+ y1: "11",
216
+ y2: "17",
217
+ key: "xtxkd"
218
+ }
219
+ ]
220
+ ]);
221
+
222
+ const sharedTransformers = [
223
+ {
224
+ name: 'fumadocs:pre-process',
225
+ line (hast) {
226
+ if (hast.children.length === 0) {
227
+ // Keep the empty lines when using grid layout
228
+ hast.children.push({
229
+ type: 'text',
230
+ value: ' '
231
+ });
232
+ }
233
+ }
234
+ }
235
+ ];
236
+
237
+ const ApiContext = /*#__PURE__*/ createContext(undefined);
238
+ function useApiContext() {
239
+ const ctx = useContext(ApiContext);
240
+ if (!ctx) throw new Error('Component must be used under <ApiProvider />');
241
+ return ctx;
242
+ }
243
+ function ApiProvider({ defaultBaseUrl, shikiOptions, children }) {
244
+ const [baseUrl, setBaseUrl] = useState(defaultBaseUrl);
245
+ useEffect(()=>{
246
+ setBaseUrl((prev)=>localStorage.getItem('apiBaseUrl') ?? prev);
247
+ }, []);
248
+ useOnChange(baseUrl, ()=>{
249
+ if (baseUrl) localStorage.setItem('apiBaseUrl', baseUrl);
250
+ });
251
+ return /*#__PURE__*/ jsx(ApiContext.Provider, {
252
+ value: useMemo(()=>({
253
+ baseUrl,
254
+ setBaseUrl,
255
+ highlight: async (lang, code)=>{
256
+ const { codeToHast } = await import('shiki/bundle/web');
257
+ return codeToHast(code, {
258
+ lang,
259
+ themes: {
260
+ light: 'github-light',
261
+ dark: 'github-dark'
262
+ },
263
+ transformers: sharedTransformers,
264
+ defaultColor: false,
265
+ ...shikiOptions
266
+ });
267
+ }
268
+ }), [
269
+ baseUrl,
270
+ shikiOptions
271
+ ]),
272
+ children: children
273
+ });
274
+ }
275
+
276
+ const SchemaContext = /*#__PURE__*/ createContext(undefined);
277
+ function useSchemaContext() {
278
+ const ctx = useContext(SchemaContext);
279
+ if (!ctx) throw new Error('Missing provider');
280
+ return ctx;
281
+ }
282
+
283
+ const APIPlayground = dynamic(()=>import('./playground-client-CcBhYwDS.js').then((mod)=>mod.APIPlayground));
284
+ function Root({ children, baseUrl, className, shikiOptions, ...props }) {
285
+ return /*#__PURE__*/ jsx("div", {
286
+ className: cn('flex flex-col gap-24 text-sm text-fd-muted-foreground', className),
287
+ ...props,
288
+ children: /*#__PURE__*/ jsx(ApiProvider, {
289
+ shikiOptions: shikiOptions,
290
+ defaultBaseUrl: baseUrl,
291
+ children: children
292
+ })
293
+ });
294
+ }
295
+ function CopyRouteButton({ className, route, ...props }) {
296
+ const { baseUrl } = useApiContext();
297
+ const [checked, onCopy] = useCopyButton(()=>{
298
+ void navigator.clipboard.writeText(`${baseUrl ?? ''}${route}`);
299
+ });
300
+ return /*#__PURE__*/ jsx("button", {
301
+ type: "button",
302
+ className: cn(buttonVariants({
303
+ color: 'ghost',
304
+ className
305
+ })),
306
+ onClick: onCopy,
307
+ "aria-label": "Copy route path",
308
+ ...props,
309
+ children: checked ? /*#__PURE__*/ jsx(Check, {
310
+ className: "size-3"
311
+ }) : /*#__PURE__*/ jsx(Copy, {
312
+ className: "size-3"
313
+ })
314
+ });
315
+ }
316
+
317
+ export { APIPlayground as A, 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 };
@@ -1,6 +1,66 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, ReactElement, MutableRefObject, HTMLAttributes } from 'react';
2
+ import { ReactNode, ComponentType, ReactElement, MutableRefObject, HTMLAttributes } from 'react';
3
3
  import { FieldPath, ControllerRenderProps, ControllerFieldState, UseFormStateReturn } from 'react-hook-form';
4
+ import { OpenAPIV3 } from 'openapi-types';
5
+ import Slugger from 'github-slugger';
6
+ import { CodeToHastOptionsCommon, CodeOptionsThemes, BuiltinTheme } from 'shiki';
7
+
8
+ /**
9
+ * Sample info of endpoint
10
+ */
11
+ interface EndpointSample {
12
+ /**
13
+ * Request URL, including path and query parameters
14
+ */
15
+ url: string;
16
+ method: string;
17
+ body?: {
18
+ schema: OpenAPIV3.SchemaObject;
19
+ mediaType: string;
20
+ sample: unknown;
21
+ };
22
+ responses: Record<string, ResponseSample>;
23
+ parameters: ParameterSample[];
24
+ }
25
+ interface ResponseSample {
26
+ mediaType: string;
27
+ sample: unknown;
28
+ schema: OpenAPIV3.SchemaObject;
29
+ }
30
+ interface ParameterSample {
31
+ name: string;
32
+ in: string;
33
+ schema: OpenAPIV3.SchemaObject;
34
+ sample: unknown;
35
+ }
36
+
37
+ interface CodeSample {
38
+ lang: string;
39
+ label: string;
40
+ source: string | ((endpoint: EndpointSample) => string | undefined) | false;
41
+ }
42
+
43
+ type Awaitable<T> = T | Promise<T>;
44
+ interface RenderContext {
45
+ renderer: Renderer;
46
+ document: OpenAPIV3.Document;
47
+ baseUrl: string;
48
+ slugger: Slugger;
49
+ /**
50
+ * Generate TypeScript definitions from response schema.
51
+ *
52
+ * Pass `false` to disable it.
53
+ *
54
+ * @param endpoint - the API endpoint
55
+ * @param code - status code
56
+ */
57
+ generateTypeScriptSchema?: ((endpoint: EndpointSample, code: string) => Awaitable<string>) | false;
58
+ /**
59
+ * Generate code samples for endpoint.
60
+ */
61
+ generateCodeSamples?: (endpoint: EndpointSample) => Awaitable<CodeSample[]>;
62
+ shikiOptions?: Omit<CodeToHastOptionsCommon, 'lang'> & CodeOptionsThemes<BuiltinTheme>;
63
+ }
4
64
 
5
65
  interface BaseRequestField {
6
66
  name: string;
@@ -59,6 +119,14 @@ interface APIPlaygroundProps {
59
119
  schemas: Record<string, RequestSchema>;
60
120
  }
61
121
 
122
+ interface ResponsesProps {
123
+ items: string[];
124
+ children: ReactNode;
125
+ }
126
+ interface ResponseProps {
127
+ value: string;
128
+ children: ReactNode;
129
+ }
62
130
  interface APIInfoProps {
63
131
  method: string;
64
132
  route: string;
@@ -71,10 +139,51 @@ interface PropertyProps {
71
139
  deprecated?: boolean;
72
140
  children?: ReactNode;
73
141
  }
142
+ interface ObjectCollapsibleProps {
143
+ name: string;
144
+ children: ReactNode;
145
+ }
146
+ interface RequestProps {
147
+ language: string;
148
+ name: string;
149
+ code: string;
150
+ }
151
+ interface ResponseTypeProps {
152
+ lang: string;
153
+ code: string;
154
+ label: string;
155
+ }
74
156
  interface RootProps {
75
157
  baseUrl?: string;
76
158
  children: ReactNode;
77
159
  }
160
+ interface Renderer {
161
+ Root: ComponentType<RootProps>;
162
+ API: ComponentType<{
163
+ children: ReactNode;
164
+ }>;
165
+ APIInfo: ComponentType<APIInfoProps>;
166
+ APIExample: ComponentType<{
167
+ children: ReactNode;
168
+ }>;
169
+ Responses: ComponentType<ResponsesProps>;
170
+ Response: ComponentType<ResponseProps>;
171
+ Requests: ComponentType<{
172
+ items: string[];
173
+ children: ReactNode;
174
+ }>;
175
+ Request: ComponentType<RequestProps>;
176
+ ResponseTypes: ComponentType<{
177
+ children: ReactNode;
178
+ }>;
179
+ ResponseType: ComponentType<ResponseTypeProps>;
180
+ /**
181
+ * Collapsible to show object schemas
182
+ */
183
+ ObjectCollapsible: ComponentType<ObjectCollapsibleProps>;
184
+ Property: ComponentType<PropertyProps>;
185
+ APIPlayground: ComponentType<APIPlaygroundProps>;
186
+ }
78
187
 
79
188
  interface FormValues {
80
189
  authorization: string;
@@ -117,14 +226,16 @@ declare const APIPlayground: react.ComponentType<APIPlaygroundProps & {
117
226
  body?: CustomField<"body", RequestSchema>;
118
227
  };
119
228
  } & HTMLAttributes<HTMLFormElement>>;
120
- declare function Root({ children, baseUrl, className, ...props }: RootProps & HTMLAttributes<HTMLDivElement>): React.ReactElement;
229
+ declare function Root({ children, baseUrl, className, shikiOptions, ...props }: RootProps & {
230
+ shikiOptions: RenderContext['shikiOptions'];
231
+ } & HTMLAttributes<HTMLDivElement>): ReactNode;
121
232
 
122
233
  declare function APIInfo({ children, className, route, badgeClassname, method, ...props }: APIInfoProps & HTMLAttributes<HTMLDivElement> & {
123
234
  badgeClassname?: string;
124
235
  }): React.ReactElement;
125
236
  declare function API({ className, children, ...props }: HTMLAttributes<HTMLDivElement>): React.ReactElement;
126
237
  declare function Property({ name, type, required, deprecated, children, }: PropertyProps): React.ReactElement;
127
- declare function APIExample({ children, className, ...props }: HTMLAttributes<HTMLDivElement>): React.ReactElement;
238
+ declare function APIExample(props: HTMLAttributes<HTMLDivElement>): React.ReactElement;
128
239
  declare function ObjectCollapsible(props: {
129
240
  name: string;
130
241
  children: ReactNode;
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 { C as CopyRouteButton } from './client-client-CUW5FVMv.js';
7
- export { A as APIPlayground, R as Root, u as useSchemaContext } from './client-client-CUW5FVMv.js';
6
+ import { f as CopyRouteButton } from './client-client-ByT1LZmz.js';
7
+ export { A as APIPlayground, R as Root, u as useSchemaContext } from './client-client-ByT1LZmz.js';
8
8
 
9
9
  const badgeVariants = cva('rounded border px-1.5 py-1 text-xs font-medium leading-[12px]', {
10
10
  variants: {
@@ -59,7 +59,10 @@ function APIInfo({ children, className, route, badgeClassname, method = 'GET', .
59
59
  ...props,
60
60
  children: [
61
61
  /*#__PURE__*/ jsxs("div", {
62
- className: cn('sticky top-24 z-20 mb-4 flex flex-row items-center gap-2 rounded-lg border bg-fd-card px-3 py-2 md:top-12 lg:top-1'),
62
+ className: cn('sticky top-[calc(var(--fd-api-info-top)+36px)] z-20 mb-4 flex flex-row items-center gap-2 rounded-lg border bg-fd-card px-3 py-2 md:top-[var(--fd-api-info-top)]'),
63
+ style: {
64
+ '--fd-api-info-top': 'calc(var(--fd-nav-height) + var(--fd-banner-height) + 4px)'
65
+ },
63
66
  children: [
64
67
  /*#__PURE__*/ jsx("span", {
65
68
  className: cn(badgeVariants({
@@ -122,11 +125,15 @@ function Property({ name, type, required, deprecated, children }) {
122
125
  ]
123
126
  });
124
127
  }
125
- function APIExample({ children, className, ...props }) {
128
+ function APIExample(props) {
126
129
  return /*#__PURE__*/ jsx("div", {
127
- className: cn('prose-no-margin md:sticky md:top-12 lg:top-1 xl:w-[400px]', className),
128
130
  ...props,
129
- children: children
131
+ className: cn('prose-no-margin md:sticky md:top-[var(--fd-api-info-top)] xl:w-[400px]', props.className),
132
+ style: {
133
+ '--fd-api-info-top': 'calc(var(--fd-nav-height) + var(--fd-banner-height) + 40px)',
134
+ ...props.style
135
+ },
136
+ children: props.children
130
137
  });
131
138
  }
132
139
  function ObjectCollapsible(props) {
@@ -1,18 +1,17 @@
1
1
  'use client';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
  import * as React from 'react';
4
- import { forwardRef, useId, createContext, useContext, useState, useCallback, useLayoutEffect, useRef, useEffect, useMemo } from 'react';
4
+ import { forwardRef, useId, createContext, useContext, useState, useCallback, useEffect, Fragment as Fragment$1, useRef, useMemo } from 'react';
5
5
  import { FormProvider, Controller, useFormContext, useFieldArray, useForm, useWatch } from 'react-hook-form';
6
- import useSWRImmutable from 'swr/immutable';
7
6
  import { Accordions, Accordion } from 'fumadocs-ui/components/accordion';
8
7
  import { cn, buttonVariants } from 'fumadocs-ui/components/api';
9
- import { u as useSchemaContext, a as useApiContext, S as SchemaContext } from './client-client-CUW5FVMv.js';
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-ByT1LZmz.js';
10
9
  import { Slot } from '@radix-ui/react-slot';
11
10
  import { cva } from 'class-variance-authority';
12
- import { CircleCheckIcon, CircleXIcon, ChevronDown, ChevronUp, Check, Trash2, Plus } from 'lucide-react';
13
11
  import { useOnChange } from 'fumadocs-core/utils/use-on-change';
14
12
  import * as SelectPrimitive from '@radix-ui/react-select';
15
13
  import * as Base from 'fumadocs-ui/components/codeblock';
14
+ import { toJsxRuntime } from 'hast-util-to-jsx-runtime';
16
15
 
17
16
  const Form = FormProvider;
18
17
  const FormFieldContext = /*#__PURE__*/ createContext({
@@ -124,7 +123,9 @@ FormDescription.displayName = 'FormDescription';
124
123
  formData.append(key, item);
125
124
  }
126
125
  }
127
- formData.set(key, JSON.stringify(prop));
126
+ if (prop && !(prop instanceof File)) {
127
+ formData.set(key, JSON.stringify(prop));
128
+ }
128
129
  }
129
130
  return formData;
130
131
  }
@@ -192,27 +193,27 @@ const statusMap = {
192
193
  400: {
193
194
  description: 'Bad Request',
194
195
  color: 'text-red-500',
195
- icon: CircleXIcon
196
+ icon: CircleX
196
197
  },
197
198
  401: {
198
199
  description: 'Unauthorized',
199
200
  color: 'text-red-500',
200
- icon: CircleXIcon
201
+ icon: CircleX
201
202
  },
202
203
  403: {
203
204
  description: 'Forbidden',
204
205
  color: 'text-red-500',
205
- icon: CircleXIcon
206
+ icon: CircleX
206
207
  },
207
208
  404: {
208
209
  description: 'Not Found',
209
210
  color: 'text-fd-muted-foreground',
210
- icon: CircleXIcon
211
+ icon: CircleX
211
212
  },
212
213
  500: {
213
214
  description: 'Internal Server Error',
214
215
  color: 'text-red-500',
215
- icon: CircleXIcon
216
+ icon: CircleX
216
217
  }
217
218
  };
218
219
  function getStatusInfo(status) {
@@ -223,20 +224,20 @@ function getStatusInfo(status) {
223
224
  return {
224
225
  description: 'Successful',
225
226
  color: 'text-green-500',
226
- icon: CircleCheckIcon
227
+ icon: CircleCheck
227
228
  };
228
229
  }
229
230
  if (status >= 400) {
230
231
  return {
231
232
  description: 'Error',
232
233
  color: 'text-red-500',
233
- icon: CircleXIcon
234
+ icon: CircleX
234
235
  };
235
236
  }
236
237
  return {
237
238
  description: 'No Description',
238
239
  color: 'text-fd-muted-foreground',
239
- icon: CircleXIcon
240
+ icon: CircleX
240
241
  };
241
242
  }
242
243
 
@@ -809,56 +810,43 @@ function ArrayInput({ fieldName, field, ...props }) {
809
810
  });
810
811
  }
811
812
 
812
- const sharedTransformers = [
813
- {
814
- name: 'fumadocs:pre-process',
815
- line (hast) {
816
- if (hast.children.length === 0) {
817
- // Keep the empty lines when using grid layout
818
- hast.children.push({
819
- type: 'text',
820
- value: ' '
821
- });
822
- }
823
- }
824
- }
825
- ];
826
-
827
- function CodeBlock({ code, lang = 'json', ...props }) {
828
- const { highlighter } = useApiContext();
829
- const [html, setHtml] = useState('');
830
- useLayoutEffect(()=>{
831
- if (!highlighter) return;
832
- const themedHtml = highlighter.codeToHtml(code, {
833
- lang,
834
- defaultColor: false,
835
- themes: {
836
- light: 'github-light',
837
- dark: 'github-dark'
838
- },
839
- transformers: sharedTransformers
813
+ function CodeBlock({ code, lang = 'json' }) {
814
+ const { highlight } = useApiContext();
815
+ const [rendered, setRendered] = useState(/*#__PURE__*/ jsx(Base.Pre, {
816
+ className: "max-h-[288px]",
817
+ children: code
818
+ }));
819
+ useEffect(()=>{
820
+ void highlight(lang, code).then((res)=>{
821
+ const output = toJsxRuntime(res, {
822
+ jsx,
823
+ jsxs,
824
+ development: false,
825
+ Fragment: Fragment$1,
826
+ components: {
827
+ pre: (props)=>/*#__PURE__*/ jsx(Base.Pre, {
828
+ className: "max-h-[288px]",
829
+ ...props,
830
+ children: props.children
831
+ })
832
+ }
833
+ });
834
+ setRendered(output);
840
835
  });
841
- setHtml(themedHtml);
842
836
  }, [
843
837
  code,
844
- lang,
845
- highlighter
838
+ highlight,
839
+ lang
846
840
  ]);
847
841
  return /*#__PURE__*/ jsx(Base.CodeBlock, {
848
842
  className: "my-0",
849
- children: /*#__PURE__*/ jsx(Base.Pre, {
850
- ...props,
851
- dangerouslySetInnerHTML: {
852
- __html: html
853
- }
854
- })
843
+ children: rendered
855
844
  });
856
845
  }
857
846
 
858
847
  function APIPlayground({ route, method = 'GET', bodyType, authorization, path = [], header = [], query = [], body, fields = {}, schemas }) {
859
848
  const { baseUrl } = useApiContext();
860
849
  const dynamicRef = useRef(new Map());
861
- const [input, setInput] = useState();
862
850
  const form = useForm({
863
851
  defaultValues: {
864
852
  authorization: authorization?.defaultValue,
@@ -868,18 +856,10 @@ function APIPlayground({ route, method = 'GET', bodyType, authorization, path =
868
856
  body: body ? getDefaultValue(body, schemas) : undefined
869
857
  }
870
858
  });
871
- const testQuery = useSWRImmutable(input ? [
872
- baseUrl,
873
- route,
874
- method,
875
- input,
876
- bodyType
877
- ] : null, async ()=>{
878
- if (!input) return;
859
+ const testQuery = useQuery(async (input)=>{
879
860
  const url = new URL(`${baseUrl ?? window.location.origin}${createUrlFromInput(route, input.path, input.query)}`);
880
- const headers = new Headers({
881
- 'Content-Type': 'application/json'
882
- });
861
+ const headers = new Headers();
862
+ if (bodyType !== 'form-data') headers.append('Content-Type', 'application/json');
883
863
  if (input.authorization) {
884
864
  headers.append('Authorization', input.authorization);
885
865
  }
@@ -898,8 +878,6 @@ function APIPlayground({ route, method = 'GET', bodyType, authorization, path =
898
878
  status: response.status,
899
879
  data
900
880
  };
901
- }, {
902
- shouldRetryOnError: false
903
881
  });
904
882
  useEffect(()=>{
905
883
  if (!authorization) return;
@@ -916,7 +894,7 @@ function APIPlayground({ route, method = 'GET', bodyType, authorization, path =
916
894
  // eslint-disable-next-line react-hooks/exhaustive-deps -- mounted only once
917
895
  }, []);
918
896
  const onSubmit = form.handleSubmit((value)=>{
919
- setInput(value);
897
+ testQuery.start(value);
920
898
  });
921
899
  function renderCustomField(fieldName, info, field, key) {
922
900
  if (field) {
@@ -1058,11 +1036,32 @@ function ResultDisplay({ data }) {
1058
1036
  children: data.status
1059
1037
  }),
1060
1038
  data.data ? /*#__PURE__*/ jsx(CodeBlock, {
1061
- code: JSON.stringify(data.data, null, 2),
1062
- className: "max-h-[288px]"
1039
+ code: JSON.stringify(data.data, null, 2)
1063
1040
  }) : null
1064
1041
  ]
1065
1042
  });
1066
1043
  }
1044
+ function useQuery(fn) {
1045
+ const [loading, setLoading] = useState(false);
1046
+ const [data, setData] = useState();
1047
+ return useMemo(()=>({
1048
+ isLoading: loading,
1049
+ data,
1050
+ start (input) {
1051
+ setLoading(true);
1052
+ void fn(input).then((res)=>{
1053
+ setData(res);
1054
+ }).catch(()=>{
1055
+ setData(undefined);
1056
+ }).finally(()=>{
1057
+ setLoading(false);
1058
+ });
1059
+ }
1060
+ }), [
1061
+ data,
1062
+ fn,
1063
+ loading
1064
+ ]);
1065
+ }
1067
1066
 
1068
1067
  export { APIPlayground };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fumadocs-openapi",
3
- "version": "5.4.14",
3
+ "version": "5.5.0",
4
4
  "description": "Generate MDX docs for your OpenAPI spec",
5
5
  "keywords": [
6
6
  "NextJs",
@@ -27,55 +27,41 @@
27
27
  },
28
28
  "main": "./dist/index.js",
29
29
  "types": "./dist/index.d.ts",
30
- "typesVersions": {
31
- "*": {
32
- ".": [
33
- "./dist/index.d.ts"
34
- ],
35
- "ui": [
36
- "./dist/ui/index.d.ts"
37
- ],
38
- "server": [
39
- "./dist/server/index.d.ts"
40
- ]
41
- }
42
- },
43
30
  "files": [
44
31
  "dist"
45
32
  ],
46
33
  "dependencies": {
47
- "@apidevtools/json-schema-ref-parser": "^11.7.0",
48
- "@fumari/json-schema-to-typescript": "^1.1.0",
49
- "@radix-ui/react-select": "^2.1.1",
34
+ "@apidevtools/json-schema-ref-parser": "^11.7.2",
35
+ "@fumari/json-schema-to-typescript": "^1.1.1",
36
+ "@radix-ui/react-select": "^2.1.2",
50
37
  "@radix-ui/react-slot": "^1.1.0",
51
38
  "class-variance-authority": "^0.7.0",
52
39
  "fast-glob": "^3.3.1",
53
40
  "github-slugger": "^2.0.0",
54
- "hast-util-to-jsx-runtime": "^2.3.0",
41
+ "hast-util-to-jsx-runtime": "^2.3.2",
55
42
  "js-yaml": "^4.1.0",
56
- "lucide-react": "^0.438.0",
57
43
  "openapi-sampler": "^1.5.1",
58
- "react-hook-form": "^7.53.0",
44
+ "react-hook-form": "^7.53.1",
59
45
  "remark": "^15.0.1",
60
- "remark-rehype": "^11.1.0",
61
- "shiki": "^1.16.2",
62
- "swr": "^2.2.5",
63
- "fumadocs-core": "13.4.10",
64
- "fumadocs-ui": "13.4.10"
46
+ "remark-rehype": "^11.1.1",
47
+ "shiki": "^1.22.0",
48
+ "fumadocs-core": "14.0.0",
49
+ "fumadocs-ui": "14.0.0"
65
50
  },
66
51
  "devDependencies": {
67
52
  "@types/js-yaml": "^4.0.9",
68
- "@types/node": "22.5.4",
53
+ "@types/node": "22.7.7",
69
54
  "@types/openapi-sampler": "^1.0.3",
70
- "@types/react": "^18.3.5",
71
- "bunchee": "^5.4.0",
72
- "next": "^14.2.8",
55
+ "@types/react": "^18.3.11",
56
+ "bunchee": "^5.5.1",
57
+ "lucide-react": "^0.453.0",
58
+ "next": "15.0.0",
73
59
  "openapi-types": "^12.1.3",
74
60
  "eslint-config-custom": "0.0.0",
75
61
  "tsconfig": "0.0.0"
76
62
  },
77
63
  "peerDependencies": {
78
- "next": ">= 14.1.0",
64
+ "next": "14.x.x || 15.x.x",
79
65
  "react": ">= 18",
80
66
  "react-dom": ">= 18"
81
67
  },
@@ -1,100 +0,0 @@
1
- 'use client';
2
- import { useContext, createContext, useState, useEffect } from 'react';
3
- import { jsx } from 'react/jsx-runtime';
4
- import { Check, Copy } from 'lucide-react';
5
- import { cn, useCopyButton, buttonVariants } from 'fumadocs-ui/components/api';
6
- import dynamic from 'next/dynamic';
7
-
8
- const ApiContext = /*#__PURE__*/ createContext({
9
- baseUrl: undefined,
10
- setBaseUrl: ()=>undefined,
11
- highlighter: null
12
- });
13
- function useApiContext() {
14
- return useContext(ApiContext);
15
- }
16
- async function initHighlighter() {
17
- const { createHighlighterCore } = await import('shiki/core');
18
- const getWasm = await import('shiki/wasm');
19
- return createHighlighterCore({
20
- themes: [
21
- import('shiki/themes/github-light.mjs'),
22
- import('shiki/themes/github-dark.mjs')
23
- ],
24
- langs: [
25
- import('shiki/langs/json.mjs')
26
- ],
27
- loadWasm: getWasm
28
- });
29
- }
30
- let highlighterInstance;
31
- function ApiProvider({ defaultBaseUrl, children }) {
32
- const [highlighter, setHighlighter] = useState(null);
33
- const [baseUrl, setBaseUrl] = useState(defaultBaseUrl);
34
- useEffect(()=>{
35
- setBaseUrl((prev)=>localStorage.getItem('apiBaseUrl') ?? prev);
36
- if (highlighterInstance) {
37
- setHighlighter(highlighterInstance);
38
- } else {
39
- void initHighlighter().then((res)=>{
40
- highlighterInstance = res;
41
- setHighlighter(res);
42
- });
43
- }
44
- }, []);
45
- useEffect(()=>{
46
- if (baseUrl) localStorage.setItem('apiBaseUrl', baseUrl);
47
- }, [
48
- baseUrl
49
- ]);
50
- return /*#__PURE__*/ jsx(ApiContext.Provider, {
51
- value: {
52
- baseUrl,
53
- setBaseUrl,
54
- highlighter
55
- },
56
- children: children
57
- });
58
- }
59
-
60
- const SchemaContext = /*#__PURE__*/ createContext(undefined);
61
- function useSchemaContext() {
62
- const ctx = useContext(SchemaContext);
63
- if (!ctx) throw new Error('Missing provider');
64
- return ctx;
65
- }
66
-
67
- const APIPlayground = dynamic(()=>import('./playground-client-CbOYGXy9.js').then((mod)=>mod.APIPlayground));
68
- function Root({ children, baseUrl, className, ...props }) {
69
- return /*#__PURE__*/ jsx("div", {
70
- className: cn('flex flex-col gap-24 text-sm text-fd-muted-foreground', className),
71
- ...props,
72
- children: /*#__PURE__*/ jsx(ApiProvider, {
73
- defaultBaseUrl: baseUrl,
74
- children: children
75
- })
76
- });
77
- }
78
- function CopyRouteButton({ className, route, ...props }) {
79
- const { baseUrl } = useApiContext();
80
- const [checked, onCopy] = useCopyButton(()=>{
81
- void navigator.clipboard.writeText(`${baseUrl ?? ''}${route}`);
82
- });
83
- return /*#__PURE__*/ jsx("button", {
84
- type: "button",
85
- className: cn(buttonVariants({
86
- color: 'ghost',
87
- className
88
- })),
89
- onClick: onCopy,
90
- "aria-label": "Copy route path",
91
- ...props,
92
- children: checked ? /*#__PURE__*/ jsx(Check, {
93
- className: "size-3"
94
- }) : /*#__PURE__*/ jsx(Copy, {
95
- className: "size-3"
96
- })
97
- });
98
- }
99
-
100
- export { APIPlayground as A, CopyRouteButton as C, Root as R, SchemaContext as S, useApiContext as a, useSchemaContext as u };