fumadocs-openapi 2.0.5 → 3.1.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +68 -20
  2. package/dist/index.js +343 -211
  3. package/package.json +3 -1
package/dist/index.d.ts CHANGED
@@ -1,28 +1,73 @@
1
1
  import { OpenAPIV3 } from 'openapi-types';
2
2
 
3
- declare function dereference(pathOrDocument: string | OpenAPIV3.Document): Promise<OpenAPIV3.Document>;
3
+ interface ResponsesProps {
4
+ items: string[];
5
+ }
6
+ interface ResponseProps {
7
+ value: string;
8
+ }
9
+ interface APIInfoProps {
10
+ method: string;
11
+ route: string;
12
+ }
13
+ interface PropertyProps {
14
+ name: string;
15
+ type: string;
16
+ required?: boolean;
17
+ deprecated?: boolean;
18
+ }
19
+ interface ObjectCollapsibleProps {
20
+ name: string;
21
+ }
22
+ interface RequestProps {
23
+ language: string;
24
+ name: string;
25
+ code: string;
26
+ }
27
+ interface Renderer {
28
+ Root: (child: string[]) => string;
29
+ API: (child: string[]) => string;
30
+ APIInfo: (props: APIInfoProps, child: string[]) => string;
31
+ APIExample: (child: string[]) => string;
32
+ Responses: (props: ResponsesProps, child: string[]) => string;
33
+ Response: (props: ResponseProps, child: string[]) => string;
34
+ Requests: (items: string[], child: string[]) => string;
35
+ Request: (props: RequestProps) => string;
36
+ ResponseTypes: (child: string[]) => string;
37
+ ExampleResponse: (json: string) => string;
38
+ TypeScriptResponse: (code: string) => string;
39
+ /**
40
+ * Collapsible to show object schemas
41
+ */
42
+ ObjectCollapsible: (props: ObjectCollapsibleProps, child: string[]) => string;
43
+ Property: (props: PropertyProps, child: string[]) => string;
44
+ }
45
+ declare const defaultRenderer: Renderer;
46
+
4
47
  interface GenerateOptions {
5
- tag?: string;
6
48
  /**
7
- * The import path of your API components, it must exports all components in `fumadocs-ui/components/api`
49
+ * The imports of your MDX components.
8
50
  *
9
- * @defaultValue `fumadocs-ui/components/api`
51
+ * If not specified, import required components from `fumadocs-ui/components/api`.
52
+ */
53
+ imports?: {
54
+ names: string[];
55
+ from: string;
56
+ }[];
57
+ /**
58
+ * Customise frontmatter
10
59
  */
11
- componentsImportPath?: string;
12
- render?: (title: string | undefined, description: string | undefined, content: string) => Partial<RenderResult>;
60
+ frontmatter?: (title: string, description: string | undefined) => Record<string, unknown>;
61
+ renderer?: Partial<Renderer>;
13
62
  }
14
- interface RenderResult {
15
- frontmatter: string;
16
- imports: string[];
63
+ interface GenerateTagOutput {
64
+ tag: string;
17
65
  content: string;
18
66
  }
19
67
  declare function generate(pathOrDocument: string | OpenAPIV3.Document, options?: GenerateOptions): Promise<string>;
20
- declare function generateTags(pathOrDocument: string | OpenAPIV3.Document, options?: Omit<GenerateOptions, 'tag'>): Promise<{
21
- tag: string;
22
- content: string;
23
- }[]>;
68
+ declare function generateTags(pathOrDocument: string | OpenAPIV3.Document, options?: GenerateOptions): Promise<GenerateTagOutput[]>;
24
69
 
25
- interface Config {
70
+ interface Config extends GenerateOptions {
26
71
  /**
27
72
  * Schema files
28
73
  */
@@ -43,13 +88,9 @@ interface Config {
43
88
  * Specify name for output file
44
89
  */
45
90
  name?: (type: 'file' | 'tag', name: string) => string;
46
- /**
47
- * Modify output file
48
- */
49
- render?: NonNullable<GenerateOptions['render']>;
50
91
  cwd?: string;
51
92
  }
52
- declare function generateFiles({ input, output, name: nameFn, per, render, cwd, }: Config): Promise<void>;
93
+ declare function generateFiles({ input, output, name: nameFn, per, cwd, ...options }: Config): Promise<void>;
53
94
 
54
95
  interface RouteInformation {
55
96
  path: string;
@@ -61,5 +102,12 @@ interface MethodInformation extends OpenAPIV3.OperationObject {
61
102
  parameters: OpenAPIV3.ParameterObject[];
62
103
  method: string;
63
104
  }
105
+ interface RenderContext {
106
+ renderer: Renderer;
107
+ document: OpenAPIV3.Document;
108
+ baseUrl: string;
109
+ }
110
+
111
+ declare function createElement(name: string, props: object, ...child: string[]): string;
64
112
 
65
- export { type Config, type GenerateOptions, type MethodInformation, type RouteInformation, dereference, generate, generateFiles, generateTags };
113
+ export { type APIInfoProps, type Config, type GenerateOptions, type GenerateTagOutput, type MethodInformation, type ObjectCollapsibleProps, type PropertyProps, type RenderContext, type Renderer, type RequestProps, type ResponseProps, type ResponsesProps, type RouteInformation, createElement, defaultRenderer, generate, generateFiles, generateTags };
package/dist/index.js CHANGED
@@ -1,12 +1,60 @@
1
1
  // src/generate.ts
2
2
  import Parser from "@apidevtools/json-schema-ref-parser";
3
3
 
4
+ // src/build-routes.ts
5
+ var methodKeys = [
6
+ "get",
7
+ "post",
8
+ "patch",
9
+ "delete",
10
+ "head",
11
+ "put"
12
+ ];
13
+ function buildRoutes(document) {
14
+ const map = /* @__PURE__ */ new Map();
15
+ for (const [path, value] of Object.entries(document.paths)) {
16
+ if (!value) continue;
17
+ const methodMap = /* @__PURE__ */ new Map();
18
+ for (const methodKey of methodKeys) {
19
+ const operation = value[methodKey];
20
+ if (!operation) continue;
21
+ const info = buildOperation(methodKey, operation);
22
+ const tags = operation.tags ?? [];
23
+ for (const tag of [...tags, "all"]) {
24
+ const list = methodMap.get(tag) ?? [];
25
+ list.push(info);
26
+ methodMap.set(tag, list);
27
+ }
28
+ }
29
+ for (const [tag, methods] of methodMap.entries()) {
30
+ const list = map.get(tag) ?? [];
31
+ list.push({
32
+ ...value,
33
+ path,
34
+ methods
35
+ });
36
+ map.set(tag, list);
37
+ }
38
+ }
39
+ return map;
40
+ }
41
+ function buildOperation(method, operation) {
42
+ return {
43
+ ...operation,
44
+ parameters: operation.parameters ?? [],
45
+ method: method.toUpperCase()
46
+ };
47
+ }
48
+
49
+ // src/render/page.ts
50
+ import { dump } from "js-yaml";
51
+
4
52
  // src/render/element.ts
5
53
  function createElement(name, props, ...child) {
6
54
  const s = [];
7
55
  const params = Object.entries(props).map(([key, value]) => `${key}={${JSON.stringify(value)}}`).join(" ");
8
56
  s.push(params.length > 0 ? `<${name} ${params}>` : `<${name}>`);
9
- s.push(...child);
57
+ s.push(...child.filter((v) => v.length > 0));
10
58
  s.push(`</${name}>`);
11
59
  return s.join("\n\n");
12
60
  }
@@ -20,48 +68,58 @@ function span(child) {
20
68
  function codeblock({ language, title }, child) {
21
69
  return [
22
70
  title ? `\`\`\`${language} title=${JSON.stringify(title)}` : `\`\`\`${language}`,
23
- child,
71
+ child.trim(),
24
72
  "```"
25
73
  ].join("\n");
26
74
  }
27
75
 
28
- // src/render/custom.ts
29
- function api(...child) {
30
- return createElement("API", {}, ...child);
31
- }
32
- function apiExample(...child) {
33
- return createElement("APIExample", {}, ...child);
34
- }
35
- function root(...child) {
36
- return createElement("Root", {}, ...child);
37
- }
38
- function apiInfo(props, ...child) {
39
- return createElement("APIInfo", props, ...child);
40
- }
41
- function accordions(...child) {
42
- return createElement("Accordions", {}, ...child);
43
- }
44
- function accordion(props, ...child) {
45
- return createElement("Accordion", props, ...child);
46
- }
47
- function tabs(props, ...child) {
48
- return createElement("Tabs", props, ...child);
49
- }
50
- function tab(props, ...child) {
51
- return createElement("Tab", props, ...child);
52
- }
53
- function property({ required = false, deprecated = false, ...props }, ...child) {
54
- return createElement(
55
- "Property",
56
- { required, deprecated, ...props },
57
- ...child
58
- );
59
- }
76
+ // src/render/renderer.ts
77
+ var defaultRenderer = {
78
+ Root: (child) => createElement("Root", {}, ...child),
79
+ API: (child) => createElement("API", {}, ...child),
80
+ APIInfo: (props, child) => createElement("APIInfo", props, ...child),
81
+ APIExample: (child) => createElement("APIExample", {}, ...child),
82
+ Responses: (props, child) => createElement("Responses", props, ...child),
83
+ Response: (props, child) => createElement("Response", props, ...child),
84
+ ResponseTypes: (child) => createElement("ResponseTypes", {}, ...child),
85
+ ExampleResponse: (json) => createElement("ExampleResponse", {}, codeblock({ language: "json" }, json)),
86
+ TypeScriptResponse: (code) => createElement(
87
+ "TypeScriptResponse",
88
+ {},
89
+ codeblock({ language: "ts" }, code)
90
+ ),
91
+ Property: (props, child) => createElement("Property", props, ...child),
92
+ ObjectCollapsible: (props, child) => createElement("ObjectCollapsible", props, ...child),
93
+ Requests: (items, child) => createElement("Requests", { items }, ...child),
94
+ Request: ({ language, code, name }) => createElement("Request", { value: name }, codeblock({ language }, code))
95
+ };
60
96
 
61
- // src/samples/index.ts
62
- import { sample } from "openapi-sampler";
97
+ // src/render/page.ts
98
+ function renderPage(title, description, content, options) {
99
+ const banner = dump({
100
+ title,
101
+ description,
102
+ ...options.frontmatter?.(title, description)
103
+ }).trim();
104
+ const finalImports = (options.imports ?? [
105
+ {
106
+ names: Object.keys(defaultRenderer),
107
+ from: "fumadocs-ui/components/api"
108
+ }
109
+ ]).map(
110
+ (item) => `import { ${item.names.join(", ")} } from ${JSON.stringify(item.from)};`
111
+ ).join("\n");
112
+ const Root = options.renderer?.Root ?? defaultRenderer.Root;
113
+ return `---
114
+ ${banner}
115
+ ---
63
116
 
64
- // src/utils.ts
117
+ ${finalImports}
118
+
119
+ ${Root(content)}`;
120
+ }
121
+
122
+ // src/utils/schema.ts
65
123
  function noRef(v) {
66
124
  return v;
67
125
  }
@@ -70,12 +128,21 @@ function getPreferredMedia(body) {
70
128
  if ("application/json" in body) return body["application/json"];
71
129
  return Object.values(body)[0];
72
130
  }
73
- function getValue(value) {
131
+ function toSampleInput(value) {
74
132
  return typeof value === "string" ? value : JSON.stringify(value, null, 2);
75
133
  }
76
134
 
77
- // src/samples/index.ts
78
- function createEndpoint(path, method, baseUrl = "https://example.com") {
135
+ // src/utils/generate-input.ts
136
+ import { sample } from "openapi-sampler";
137
+ function generateInput(method, schema) {
138
+ return sample(schema, {
139
+ skipReadOnly: method !== "GET",
140
+ skipWriteOnly: method === "GET"
141
+ });
142
+ }
143
+
144
+ // src/endpoint.ts
145
+ function createEndpoint(path, method, baseUrl) {
79
146
  const params = [];
80
147
  const responses = {};
81
148
  for (const param of method.parameters) {
@@ -102,33 +169,28 @@ function createEndpoint(path, method, baseUrl = "https://example.com") {
102
169
  let pathWithParameters = path;
103
170
  const queryParams = new URLSearchParams();
104
171
  for (const param of params) {
105
- const value = generateSample(method.method, param.schema);
106
- if (param.in === "query") queryParams.append(param.name, getValue(value));
172
+ const value = generateInput(method.method, param.schema);
173
+ if (param.in === "query")
174
+ queryParams.append(param.name, toSampleInput(value));
107
175
  if (param.in === "path")
108
176
  pathWithParameters = pathWithParameters.replace(
109
177
  `{${param.name}}`,
110
- getValue(value)
178
+ toSampleInput(value)
111
179
  );
112
180
  }
113
181
  return {
114
182
  url: new URL(pathWithParameters, baseUrl).toString(),
115
- body: bodySchema ? generateSample(method.method, bodySchema) : void 0,
183
+ body: bodySchema ? generateInput(method.method, bodySchema) : void 0,
116
184
  responses,
117
185
  method: method.method,
118
186
  parameters: params
119
187
  };
120
188
  }
121
- function generateSample(method, schema) {
122
- return sample(schema, {
123
- skipReadOnly: method !== "GET",
124
- skipWriteOnly: method === "GET"
125
- });
126
- }
127
189
 
128
- // src/samples/response.ts
190
+ // src/utils/generate-response.ts
129
191
  function getExampleResponse(endpoint, code) {
130
192
  if (code in endpoint.responses) {
131
- const value = generateSample(
193
+ const value = generateInput(
132
194
  endpoint.method,
133
195
  endpoint.responses[code].schema
134
196
  );
@@ -136,38 +198,66 @@ function getExampleResponse(endpoint, code) {
136
198
  }
137
199
  }
138
200
 
139
- // src/samples/typescript.ts
140
- import { compile } from "json-schema-to-typescript";
141
- async function getTypescript(endpoint, code) {
142
- if (code in endpoint.responses) {
143
- return compile(endpoint.responses[code].schema, "Response", {
144
- bannerComment: "",
145
- additionalProperties: false,
146
- format: true,
147
- enableConstEnums: false
148
- });
149
- }
150
- }
151
-
152
- // src/samples/curl.ts
201
+ // src/requests/curl.ts
153
202
  function getSampleRequest(endpoint) {
154
203
  const s = [];
155
204
  s.push(`curl -X ${endpoint.method} "${endpoint.url}"`);
156
205
  for (const param of endpoint.parameters) {
157
206
  if (param.in === "header") {
158
- const value = generateSample(endpoint.method, param.schema);
159
- const header = `${param.name}: ${getValue2(value)}`;
207
+ const value = generateInput(endpoint.method, param.schema);
208
+ const header = `${param.name}: ${toSampleInput(value)}`;
160
209
  s.push(`-H "${header}"`);
161
210
  }
162
211
  if (param.in === "formData") {
163
212
  console.log("Request example for form data is not supported");
164
213
  }
165
214
  }
166
- if (endpoint.body) s.push(`-d '${getValue2(endpoint.body)}'`);
215
+ if (endpoint.body) s.push(`-d '${toSampleInput(endpoint.body)}'`);
167
216
  return s.join(" \\\n ");
168
217
  }
169
- function getValue2(value) {
170
- return typeof value === "string" ? value : JSON.stringify(value, null, 2);
218
+
219
+ // src/requests/javascript.ts
220
+ function getSampleRequest2(endpoint) {
221
+ const s = [];
222
+ const options = /* @__PURE__ */ new Map();
223
+ const headers = {};
224
+ const formData = {};
225
+ for (const param of endpoint.parameters) {
226
+ if (param.in === "header") {
227
+ headers[param.name] = generateInput(endpoint.method, param.schema);
228
+ }
229
+ if (param.in === "formData") {
230
+ formData[param.name] = generateInput(endpoint.method, param.schema);
231
+ }
232
+ }
233
+ options.set("method", JSON.stringify(endpoint.method));
234
+ if (Object.keys(headers).length > 0) {
235
+ options.set("headers", JSON.stringify(headers, void 0, 2));
236
+ }
237
+ if (Object.keys(formData).length > 0) {
238
+ s.push(`const formData = new FormData();`);
239
+ for (const [key, value] of Object.entries(formData))
240
+ s.push(`formData.set(${key}, ${JSON.stringify(value)}`);
241
+ options.set("body", "formData");
242
+ }
243
+ const optionsStr = Array.from(options.entries()).map(([k, v]) => ` ${k}: ${v}`).join(",\n");
244
+ s.push(`fetch(${JSON.stringify(endpoint.url)}, {
245
+ ${optionsStr}
246
+ });`);
247
+ return s.join("\n\n");
248
+ }
249
+
250
+ // src/utils/get-typescript-schema.ts
251
+ import { compile } from "json-schema-to-typescript";
252
+ async function getTypescriptSchema(endpoint, code) {
253
+ if (code in endpoint.responses) {
254
+ return compile(endpoint.responses[code].schema, "Response", {
255
+ bannerComment: "",
256
+ additionalProperties: false,
257
+ format: true,
258
+ enableConstEnums: false
259
+ });
260
+ }
171
261
  }
172
262
 
173
263
  // src/render/schema.ts
@@ -184,22 +274,26 @@ var keys = {
184
274
  function isObject(schema) {
185
275
  return schema.type === "object" || schema.properties !== void 0;
186
276
  }
187
- function schemaElement(name, schema, { parseObject, ...ctx }) {
277
+ function schemaElement(name, schema, ctx) {
188
278
  if (schema.readOnly && !ctx.readOnly) return "";
189
279
  if (schema.writeOnly && !ctx.writeOnly) return "";
280
+ const { renderer } = ctx.render;
190
281
  const child = [];
191
282
  function field(key, value) {
192
283
  child.push(span(`${key}: \`${value}\``));
193
284
  }
194
- if (isObject(schema) && parseObject) {
285
+ if (isObject(schema) && ctx.parseObject) {
195
286
  const { additionalProperties, properties } = schema;
196
287
  if (additionalProperties) {
197
288
  if (additionalProperties === true) {
198
289
  child.push(
199
- property({
200
- name: "[key: string]",
201
- type: "any"
202
- })
290
+ renderer.Property(
291
+ {
292
+ name: "[key: string]",
293
+ type: "any"
294
+ },
295
+ []
296
+ )
203
297
  );
204
298
  } else {
205
299
  child.push(
@@ -235,28 +329,25 @@ function schemaElement(name, schema, { parseObject, ...ctx }) {
235
329
  );
236
330
  }
237
331
  const resolved = resolveObjectType(schema);
238
- if (resolved && !parseObject) {
332
+ if (resolved && !ctx.parseObject) {
239
333
  child.push(
240
- accordions(
241
- accordion(
242
- { title: "Object Type" },
243
- schemaElement(name, resolved, {
244
- ...ctx,
245
- parseObject: true,
246
- required: false
247
- })
248
- )
249
- )
334
+ renderer.ObjectCollapsible({ name }, [
335
+ schemaElement(name, resolved, {
336
+ ...ctx,
337
+ parseObject: true,
338
+ required: false
339
+ })
340
+ ])
250
341
  );
251
342
  }
252
- return property(
343
+ return renderer.Property(
253
344
  {
254
345
  name,
255
346
  type: getSchemaType(schema),
256
347
  required: ctx.required,
257
348
  deprecated: schema.deprecated
258
349
  },
259
- ...child
350
+ child
260
351
  );
261
352
  }
262
353
  function resolveObjectType(schema) {
@@ -284,13 +375,18 @@ function getSchemaType(schema) {
284
375
  }
285
376
 
286
377
  // src/render/operation.ts
287
- async function renderOperation(path, method, baseUrl) {
378
+ async function renderOperation(path, method, ctx) {
379
+ const body = noRef(method.requestBody);
380
+ const security = method.security ?? ctx.document.security;
288
381
  const info = [];
289
382
  const example = [];
290
383
  const title = method.summary ?? method.operationId;
291
384
  if (title) info.push(`## ${title}`);
292
385
  if (method.description) info.push(p(method.description));
293
- const body = noRef(method.requestBody);
386
+ if (security) {
387
+ info.push("### Authorization");
388
+ info.push(getAuthSection(security, ctx));
389
+ }
294
390
  if (body) {
295
391
  const bodySchema = getPreferredMedia(body.content)?.schema;
296
392
  if (!bodySchema) throw new Error();
@@ -301,12 +397,13 @@ async function renderOperation(path, method, baseUrl) {
301
397
  parseObject: true,
302
398
  readOnly: method.method === "GET",
303
399
  writeOnly: method.method !== "GET",
304
- required: body.required ?? false
400
+ required: body.required ?? false,
401
+ render: ctx
305
402
  })
306
403
  );
307
404
  }
308
405
  const parameterGroups = /* @__PURE__ */ new Map();
309
- const endpoint = createEndpoint(path, method, baseUrl);
406
+ const endpoint = createEndpoint(path, method, ctx.baseUrl);
310
407
  for (const param of method.parameters) {
311
408
  const schema = noRef(
312
409
  param.schema ?? getPreferredMedia(param.content ?? {})?.schema
@@ -323,7 +420,8 @@ async function renderOperation(path, method, baseUrl) {
323
420
  parseObject: false,
324
421
  readOnly: method.method === "GET",
325
422
  writeOnly: method.method !== "GET",
326
- required: param.required ?? false
423
+ required: param.required ?? false,
424
+ render: ctx
327
425
  }
328
426
  );
329
427
  const groupName = {
@@ -341,13 +439,94 @@ async function renderOperation(path, method, baseUrl) {
341
439
  }
342
440
  info.push(getResponseTable(method));
343
441
  example.push(
344
- codeblock({ language: "bash", title: "curl" }, getSampleRequest(endpoint))
345
- );
346
- example.push(await getResponseTabs(endpoint, method));
347
- return api(
348
- apiInfo({ method: method.method, route: path }, ...info),
349
- apiExample(...example)
442
+ ctx.renderer.Requests(
443
+ ["cURL", "JavaScript"],
444
+ [
445
+ ctx.renderer.Request({
446
+ name: "cURL",
447
+ code: getSampleRequest(endpoint),
448
+ language: "bash"
449
+ }),
450
+ ctx.renderer.Request({
451
+ name: "JavaScript",
452
+ code: getSampleRequest2(endpoint),
453
+ language: "js"
454
+ })
455
+ ]
456
+ )
350
457
  );
458
+ example.push(await getResponseTabs(endpoint, method, ctx));
459
+ return ctx.renderer.API([
460
+ ctx.renderer.APIInfo({ method: method.method, route: path }, info),
461
+ ctx.renderer.APIExample(example)
462
+ ]);
463
+ }
464
+ function getAuthSection(requirements, { document, renderer }) {
465
+ const info = [];
466
+ const schemas = document.components?.securitySchemes ?? {};
467
+ for (const requirement of requirements) {
468
+ if (info.length > 0) info.push(`---`);
469
+ for (const [name, scopes] of Object.entries(requirement)) {
470
+ if (!(name in schemas)) continue;
471
+ const schema = noRef(schemas[name]);
472
+ if (schema.type === "http") {
473
+ info.push(
474
+ renderer.Property(
475
+ {
476
+ name: "Authorization",
477
+ type: {
478
+ basic: "Basic <token>",
479
+ bearer: "Bearer <token>"
480
+ }[schema.scheme] ?? "<token>",
481
+ required: true
482
+ },
483
+ [p(schema.description), `In: \`header\``]
484
+ )
485
+ );
486
+ }
487
+ if (schema.type === "oauth2") {
488
+ info.push(
489
+ renderer.Property(
490
+ {
491
+ name: "Authorization",
492
+ type: "Bearer <token>",
493
+ required: true
494
+ },
495
+ [
496
+ p(schema.description),
497
+ `In: \`header\``,
498
+ `Scope: \`${scopes.length > 0 ? scopes.join(", ") : "none"}\``
499
+ ]
500
+ )
501
+ );
502
+ }
503
+ if (schema.type === "apiKey") {
504
+ info.push(
505
+ renderer.Property(
506
+ {
507
+ name: schema.name,
508
+ type: "<token>",
509
+ required: true
510
+ },
511
+ [p(schema.description), `In: \`${schema.in}\``]
512
+ )
513
+ );
514
+ }
515
+ if (schema.type === "openIdConnect") {
516
+ info.push(
517
+ renderer.Property(
518
+ {
519
+ name: "OpenID Connect",
520
+ type: "<token>",
521
+ required: true
522
+ },
523
+ [p(schema.description)]
524
+ )
525
+ );
526
+ }
527
+ }
528
+ }
529
+ return info.join("\n\n");
351
530
  }
352
531
  function getResponseTable(operation) {
353
532
  const table = [];
@@ -358,131 +537,86 @@ function getResponseTable(operation) {
358
537
  });
359
538
  return table.join("\n");
360
539
  }
361
- async function getResponseTabs(endpoint, operation) {
540
+ async function getResponseTabs(endpoint, operation, { renderer }) {
362
541
  const items = [];
363
542
  const child = [];
364
- for (const [code, _] of Object.entries(operation.responses)) {
543
+ for (const code of Object.keys(operation.responses)) {
365
544
  const example = getExampleResponse(endpoint, code);
366
- const ts = await getTypescript(endpoint, code);
545
+ const ts = await getTypescriptSchema(endpoint, code);
367
546
  const description = code in endpoint.responses ? endpoint.responses[code].schema.description : void 0;
368
547
  if (example && ts) {
369
548
  items.push(code);
370
549
  child.push(
371
- tab(
372
- { value: code },
550
+ renderer.Response({ value: code }, [
373
551
  p(description),
374
- codeblock({ language: "json", title: "Example Response" }, example),
375
- accordions(
376
- accordion(
377
- { title: "Typescript Definition" },
378
- codeblock({ language: "ts" }, ts)
379
- )
380
- )
381
- )
552
+ renderer.ResponseTypes([
553
+ renderer.ExampleResponse(example),
554
+ renderer.TypeScriptResponse(ts)
555
+ ])
556
+ ])
382
557
  );
383
558
  }
384
559
  }
385
560
  if (items.length === 0) return "";
386
- return tabs(
561
+ return renderer.Responses(
387
562
  {
388
563
  items
389
564
  },
390
- ...child
565
+ child
391
566
  );
392
567
  }
393
568
 
394
569
  // src/generate.ts
395
- async function dereference(pathOrDocument) {
396
- return await Parser.dereference(pathOrDocument);
397
- }
398
570
  async function generate(pathOrDocument, options = {}) {
399
- const document = await dereference(pathOrDocument);
400
- const tag = options.tag ? document.tags?.find((item) => item.name === options.tag) : void 0;
401
- const routes = Object.entries(document.paths).map(
402
- ([key, value]) => {
403
- if (!value) throw new Error("Invalid schema");
404
- const methodKeys = [
405
- "get",
406
- "post",
407
- "patch",
408
- "delete",
409
- "head",
410
- "put"
411
- ];
412
- const methods = [];
413
- for (const methodKey of methodKeys) {
414
- const operation = value[methodKey];
415
- if (!operation) continue;
416
- if (tag && !operation.tags?.includes(tag.name)) continue;
417
- methods.push(buildOperation(methodKey, operation));
418
- }
419
- return {
420
- ...value,
421
- path: key,
422
- methods
423
- };
424
- }
425
- );
426
- const serverUrl = document.servers?.[0].url;
427
- const s = [];
571
+ const document = await Parser.dereference(pathOrDocument);
572
+ const routes = buildRoutes(document).get("all") ?? [];
573
+ const ctx = getContext(document, options);
574
+ const child = [];
428
575
  for (const route of routes) {
429
576
  for (const method of route.methods) {
430
- s.push(await renderOperation(route.path, method, serverUrl));
577
+ child.push(await renderOperation(route.path, method, ctx));
431
578
  }
432
579
  }
433
- return render(
434
- tag?.name ?? document.info.title,
435
- tag?.description ?? document.info.description,
436
- root(...s),
580
+ return renderPage(
581
+ document.info.title,
582
+ document.info.description,
583
+ child,
437
584
  options
438
585
  );
439
586
  }
440
587
  async function generateTags(pathOrDocument, options = {}) {
441
- const document = await dereference(pathOrDocument);
442
- const results = document.tags?.map(async (tag) => {
443
- return {
444
- tag: tag.name,
445
- content: await generate(document, {
446
- tag: tag.name,
447
- ...options
448
- })
449
- };
450
- });
451
- return Promise.all(results ?? []);
452
- }
453
- function render(title, description, content, {
454
- render: fn,
455
- componentsImportPath = "fumadocs-ui/components/api"
456
- }) {
457
- const result = fn?.(title, description, content) ?? {};
458
- const rendered = {
459
- frontmatter: result.frontmatter ?? [
460
- "---",
461
- title && `title: ${title}`,
462
- description && `description: |
463
- ${description.split("\n").join("\n ")}
464
- `,
465
- "---"
466
- ].filter(Boolean).join("\n"),
467
- imports: result.imports ?? [
468
- `import { Root, API, APIInfo, APIExample, Property } from '${componentsImportPath}'`,
469
- `import { Tabs, Tab } from 'fumadocs-ui/components/tabs'`,
470
- `import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';`
471
- ],
472
- content: result.content ?? content
473
- };
474
- return [rendered.frontmatter, rendered.imports.join("\n"), rendered.content].filter(Boolean).join("\n\n");
588
+ const document = await Parser.dereference(pathOrDocument);
589
+ const tags = Array.from(buildRoutes(document).entries());
590
+ const ctx = getContext(document, options);
591
+ return await Promise.all(
592
+ tags.filter(([tag]) => tag !== "all").map(async ([tag, routes]) => {
593
+ const info = document.tags?.find((t) => t.name === tag);
594
+ const child = [];
595
+ for (const route of routes) {
596
+ for (const method of route.methods) {
597
+ child.push(await renderOperation(route.path, method, ctx));
598
+ }
599
+ }
600
+ return {
601
+ tag,
602
+ content: renderPage(tag, info?.description, child, options)
603
+ };
604
+ })
605
+ );
475
606
  }
476
- function buildOperation(method, operation) {
607
+ function getContext(document, options) {
477
608
  return {
478
- ...operation,
479
- parameters: operation.parameters ?? [],
480
- method: method.toUpperCase()
609
+ document,
610
+ renderer: {
611
+ ...defaultRenderer,
612
+ ...options.renderer
613
+ },
614
+ baseUrl: document.servers?.[0].url ?? "https://example.com"
481
615
  };
482
616
  }
483
617
 
484
618
  // src/generate-file.ts
485
- import { mkdirSync, writeFileSync } from "node:fs";
619
+ import { mkdir, writeFile } from "node:fs/promises";
486
620
  import { dirname, join, parse } from "node:path";
487
621
  import fg from "fast-glob";
488
622
  async function generateFiles({
@@ -490,13 +624,10 @@ async function generateFiles({
490
624
  output,
491
625
  name: nameFn,
492
626
  per = "file",
493
- render: render2,
494
- cwd = process.cwd()
627
+ cwd = process.cwd(),
628
+ ...options
495
629
  }) {
496
630
  const outputDir = join(cwd, output);
497
- const options = {
498
- render: render2
499
- };
500
631
  const resolvedInputs = await fg.glob(input, { absolute: true, cwd });
501
632
  await Promise.all(
502
633
  resolvedInputs.map(async (path) => {
@@ -505,27 +636,28 @@ async function generateFiles({
505
636
  if (per === "file") {
506
637
  const outPath = join(outputDir, `${filename}.mdx`);
507
638
  const result = await generate(path, options);
508
- write(outPath, result);
639
+ await write(outPath, result);
640
+ console.log(`Generated: ${outPath}`);
641
+ return;
642
+ }
643
+ const results = await generateTags(path, options);
644
+ for (const result of results) {
645
+ let tagName = result.tag;
646
+ tagName = nameFn?.("tag", tagName) ?? tagName.toLowerCase().replace(/\s+/g, "-");
647
+ const outPath = join(outputDir, filename, `${tagName}.mdx`);
648
+ await write(outPath, result.content);
509
649
  console.log(`Generated: ${outPath}`);
510
- } else {
511
- const results = await generateTags(path, options);
512
- results.forEach((result) => {
513
- let tagName = result.tag;
514
- tagName = nameFn?.("tag", tagName) ?? tagName.toLowerCase().replace(/\s+/g, "-");
515
- const outPath = join(outputDir, filename, `${tagName}.mdx`);
516
- write(outPath, result.content);
517
- console.log(`Generated: ${outPath}`);
518
- });
519
650
  }
520
651
  })
521
652
  );
522
653
  }
523
- function write(path, content) {
524
- mkdirSync(dirname(path), { recursive: true });
525
- writeFileSync(path, content);
654
+ async function write(path, content) {
655
+ await mkdir(dirname(path), { recursive: true });
656
+ await writeFile(path, content);
526
657
  }
527
658
  export {
528
- dereference,
659
+ createElement,
660
+ defaultRenderer,
529
661
  generate,
530
662
  generateFiles,
531
663
  generateTags
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fumadocs-openapi",
3
- "version": "2.0.5",
3
+ "version": "3.1.0",
4
4
  "description": "Generate MDX docs for your OpenAPI spec",
5
5
  "keywords": [
6
6
  "NextJs",
@@ -19,10 +19,12 @@
19
19
  "dependencies": {
20
20
  "@apidevtools/json-schema-ref-parser": "^11.6.4",
21
21
  "fast-glob": "^3.3.1",
22
+ "js-yaml": "^4.1.0",
22
23
  "json-schema-to-typescript": "^14.0.5",
23
24
  "openapi-sampler": "^1.5.1"
24
25
  },
25
26
  "devDependencies": {
27
+ "@types/js-yaml": "^4.0.9",
26
28
  "@types/node": "18.17.5",
27
29
  "@types/openapi-sampler": "^1.0.3",
28
30
  "openapi-types": "^12.1.3",