fumadocs-openapi 3.0.0 → 3.1.1

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
@@ -19,6 +19,11 @@ interface PropertyProps {
19
19
  interface ObjectCollapsibleProps {
20
20
  name: string;
21
21
  }
22
+ interface RequestProps {
23
+ language: string;
24
+ name: string;
25
+ code: string;
26
+ }
22
27
  interface Renderer {
23
28
  Root: (child: string[]) => string;
24
29
  API: (child: string[]) => string;
@@ -26,6 +31,8 @@ interface Renderer {
26
31
  APIExample: (child: string[]) => string;
27
32
  Responses: (props: ResponsesProps, child: string[]) => string;
28
33
  Response: (props: ResponseProps, child: string[]) => string;
34
+ Requests: (items: string[], child: string[]) => string;
35
+ Request: (props: RequestProps) => string;
29
36
  ResponseTypes: (child: string[]) => string;
30
37
  ExampleResponse: (json: string) => string;
31
38
  TypeScriptResponse: (code: string) => string;
@@ -57,7 +64,12 @@ interface GenerateTagOutput {
57
64
  tag: string;
58
65
  content: string;
59
66
  }
67
+ interface GenerateOperationOutput {
68
+ id: string;
69
+ content: string;
70
+ }
60
71
  declare function generate(pathOrDocument: string | OpenAPIV3.Document, options?: GenerateOptions): Promise<string>;
72
+ declare function generateOperations(pathOrDocument: string | OpenAPIV3.Document, options?: GenerateOptions): Promise<GenerateOperationOutput[]>;
61
73
  declare function generateTags(pathOrDocument: string | OpenAPIV3.Document, options?: GenerateOptions): Promise<GenerateTagOutput[]>;
62
74
 
63
75
  interface Config extends GenerateOptions {
@@ -76,7 +88,7 @@ interface Config extends GenerateOptions {
76
88
  *
77
89
  * @defaultValue file
78
90
  */
79
- per?: 'tag' | 'file';
91
+ per?: 'tag' | 'file' | 'operation';
80
92
  /**
81
93
  * Specify name for output file
82
94
  */
@@ -97,9 +109,10 @@ interface MethodInformation extends OpenAPIV3.OperationObject {
97
109
  }
98
110
  interface RenderContext {
99
111
  renderer: Renderer;
112
+ document: OpenAPIV3.Document;
100
113
  baseUrl: string;
101
114
  }
102
115
 
103
116
  declare function createElement(name: string, props: object, ...child: string[]): string;
104
117
 
105
- export { type APIInfoProps, type Config, type GenerateOptions, type GenerateTagOutput, type MethodInformation, type ObjectCollapsibleProps, type PropertyProps, type RenderContext, type Renderer, type ResponseProps, type ResponsesProps, type RouteInformation, createElement, defaultRenderer, generate, generateFiles, generateTags };
118
+ export { type APIInfoProps, type Config, type GenerateOperationOutput, 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, generateOperations, generateTags };
package/dist/index.js CHANGED
@@ -72,6 +72,9 @@ function codeblock({ language, title }, child) {
72
72
  "```"
73
73
  ].join("\n");
74
74
  }
75
+ function heading(depth, child) {
76
+ return `${"#".repeat(depth)} ${child.trim()}`;
77
+ }
75
78
 
76
79
  // src/render/renderer.ts
77
80
  var defaultRenderer = {
@@ -89,7 +92,9 @@ var defaultRenderer = {
89
92
  codeblock({ language: "ts" }, code)
90
93
  ),
91
94
  Property: (props, child) => createElement("Property", props, ...child),
92
- ObjectCollapsible: (props, child) => createElement("ObjectCollapsible", props, ...child)
95
+ ObjectCollapsible: (props, child) => createElement("ObjectCollapsible", props, ...child),
96
+ Requests: (items, child) => createElement("Requests", { items }, ...child),
97
+ Request: ({ language, code, name }) => createElement("Request", { value: name }, codeblock({ language }, code))
93
98
  };
94
99
 
95
100
  // src/render/page.ts
@@ -117,10 +122,7 @@ ${finalImports}
117
122
  ${Root(content)}`;
118
123
  }
119
124
 
120
- // src/samples/index.ts
121
- import { sample } from "openapi-sampler";
122
-
123
- // src/utils.ts
125
+ // src/utils/schema.ts
124
126
  function noRef(v) {
125
127
  return v;
126
128
  }
@@ -129,11 +131,20 @@ function getPreferredMedia(body) {
129
131
  if ("application/json" in body) return body["application/json"];
130
132
  return Object.values(body)[0];
131
133
  }
132
- function getValue(value) {
134
+ function toSampleInput(value) {
133
135
  return typeof value === "string" ? value : JSON.stringify(value, null, 2);
134
136
  }
135
137
 
136
- // src/samples/index.ts
138
+ // src/utils/generate-input.ts
139
+ import { sample } from "openapi-sampler";
140
+ function generateInput(method, schema) {
141
+ return sample(schema, {
142
+ skipReadOnly: method !== "GET",
143
+ skipWriteOnly: method === "GET"
144
+ });
145
+ }
146
+
147
+ // src/endpoint.ts
137
148
  function createEndpoint(path, method, baseUrl) {
138
149
  const params = [];
139
150
  const responses = {};
@@ -161,33 +172,28 @@ function createEndpoint(path, method, baseUrl) {
161
172
  let pathWithParameters = path;
162
173
  const queryParams = new URLSearchParams();
163
174
  for (const param of params) {
164
- const value = generateSample(method.method, param.schema);
165
- if (param.in === "query") queryParams.append(param.name, getValue(value));
175
+ const value = generateInput(method.method, param.schema);
176
+ if (param.in === "query")
177
+ queryParams.append(param.name, toSampleInput(value));
166
178
  if (param.in === "path")
167
179
  pathWithParameters = pathWithParameters.replace(
168
180
  `{${param.name}}`,
169
- getValue(value)
181
+ toSampleInput(value)
170
182
  );
171
183
  }
172
184
  return {
173
185
  url: new URL(pathWithParameters, baseUrl).toString(),
174
- body: bodySchema ? generateSample(method.method, bodySchema) : void 0,
186
+ body: bodySchema ? generateInput(method.method, bodySchema) : void 0,
175
187
  responses,
176
188
  method: method.method,
177
189
  parameters: params
178
190
  };
179
191
  }
180
- function generateSample(method, schema) {
181
- return sample(schema, {
182
- skipReadOnly: method !== "GET",
183
- skipWriteOnly: method === "GET"
184
- });
185
- }
186
192
 
187
- // src/samples/response.ts
193
+ // src/utils/generate-response.ts
188
194
  function getExampleResponse(endpoint, code) {
189
195
  if (code in endpoint.responses) {
190
- const value = generateSample(
196
+ const value = generateInput(
191
197
  endpoint.method,
192
198
  endpoint.responses[code].schema
193
199
  );
@@ -195,38 +201,66 @@ function getExampleResponse(endpoint, code) {
195
201
  }
196
202
  }
197
203
 
198
- // src/samples/typescript.ts
199
- import { compile } from "json-schema-to-typescript";
200
- async function getTypescript(endpoint, code) {
201
- if (code in endpoint.responses) {
202
- return compile(endpoint.responses[code].schema, "Response", {
203
- bannerComment: "",
204
- additionalProperties: false,
205
- format: true,
206
- enableConstEnums: false
207
- });
208
- }
209
- }
210
-
211
- // src/samples/curl.ts
204
+ // src/requests/curl.ts
212
205
  function getSampleRequest(endpoint) {
213
206
  const s = [];
214
207
  s.push(`curl -X ${endpoint.method} "${endpoint.url}"`);
215
208
  for (const param of endpoint.parameters) {
216
209
  if (param.in === "header") {
217
- const value = generateSample(endpoint.method, param.schema);
218
- const header = `${param.name}: ${getValue2(value)}`;
210
+ const value = generateInput(endpoint.method, param.schema);
211
+ const header = `${param.name}: ${toSampleInput(value)}`;
219
212
  s.push(`-H "${header}"`);
220
213
  }
221
214
  if (param.in === "formData") {
222
215
  console.log("Request example for form data is not supported");
223
216
  }
224
217
  }
225
- if (endpoint.body) s.push(`-d '${getValue2(endpoint.body)}'`);
218
+ if (endpoint.body) s.push(`-d '${toSampleInput(endpoint.body)}'`);
226
219
  return s.join(" \\\n ");
227
220
  }
228
- function getValue2(value) {
229
- return typeof value === "string" ? value : JSON.stringify(value, null, 2);
221
+
222
+ // src/requests/javascript.ts
223
+ function getSampleRequest2(endpoint) {
224
+ const s = [];
225
+ const options = /* @__PURE__ */ new Map();
226
+ const headers = {};
227
+ const formData = {};
228
+ for (const param of endpoint.parameters) {
229
+ if (param.in === "header") {
230
+ headers[param.name] = generateInput(endpoint.method, param.schema);
231
+ }
232
+ if (param.in === "formData") {
233
+ formData[param.name] = generateInput(endpoint.method, param.schema);
234
+ }
235
+ }
236
+ options.set("method", JSON.stringify(endpoint.method));
237
+ if (Object.keys(headers).length > 0) {
238
+ options.set("headers", JSON.stringify(headers, void 0, 2));
239
+ }
240
+ if (Object.keys(formData).length > 0) {
241
+ s.push(`const formData = new FormData();`);
242
+ for (const [key, value] of Object.entries(formData))
243
+ s.push(`formData.set(${key}, ${JSON.stringify(value)}`);
244
+ options.set("body", "formData");
245
+ }
246
+ const optionsStr = Array.from(options.entries()).map(([k, v]) => ` ${k}: ${v}`).join(",\n");
247
+ s.push(`fetch(${JSON.stringify(endpoint.url)}, {
248
+ ${optionsStr}
249
+ });`);
250
+ return s.join("\n\n");
251
+ }
252
+
253
+ // src/utils/get-typescript-schema.ts
254
+ import { compile } from "json-schema-to-typescript";
255
+ async function getTypescriptSchema(endpoint, code) {
256
+ if (code in endpoint.responses) {
257
+ return compile(endpoint.responses[code].schema, "Response", {
258
+ bannerComment: "",
259
+ additionalProperties: false,
260
+ format: true,
261
+ enableConstEnums: false
262
+ });
263
+ }
230
264
  }
231
265
 
232
266
  // src/render/schema.ts
@@ -344,18 +378,24 @@ function getSchemaType(schema) {
344
378
  }
345
379
 
346
380
  // src/render/operation.ts
347
- async function renderOperation(path, method, ctx) {
381
+ async function renderOperation(path, method, ctx, noTitle = false) {
382
+ let level = 2;
383
+ const body = noRef(method.requestBody);
384
+ const security = method.security ?? ctx.document.security;
348
385
  const info = [];
349
386
  const example = [];
350
387
  const title = method.summary ?? method.operationId;
351
- if (title) info.push(`## ${title}`);
388
+ if (title && !noTitle) info.push(heading(level++, title));
352
389
  if (method.description) info.push(p(method.description));
353
- const body = noRef(method.requestBody);
390
+ if (security) {
391
+ info.push(heading(level, "Authorization"));
392
+ info.push(getAuthSection(security, ctx));
393
+ }
354
394
  if (body) {
355
395
  const bodySchema = getPreferredMedia(body.content)?.schema;
356
396
  if (!bodySchema) throw new Error();
357
397
  info.push(
358
- `### Request Body${!body.required ? " (Optional)" : ""}`,
398
+ heading(level, `Request Body ${!body.required ? "(Optional)" : ""}`),
359
399
  p(body.description),
360
400
  schemaElement("body", noRef(bodySchema), {
361
401
  parseObject: true,
@@ -399,11 +439,25 @@ async function renderOperation(path, method, ctx) {
399
439
  parameterGroups.set(groupName, group);
400
440
  }
401
441
  for (const [group, parameters] of Array.from(parameterGroups.entries())) {
402
- info.push(`### ${group}`, ...parameters);
442
+ info.push(heading(level, group), ...parameters);
403
443
  }
404
444
  info.push(getResponseTable(method));
405
445
  example.push(
406
- codeblock({ language: "bash", title: "curl" }, getSampleRequest(endpoint))
446
+ ctx.renderer.Requests(
447
+ ["cURL", "JavaScript"],
448
+ [
449
+ ctx.renderer.Request({
450
+ name: "cURL",
451
+ code: getSampleRequest(endpoint),
452
+ language: "bash"
453
+ }),
454
+ ctx.renderer.Request({
455
+ name: "JavaScript",
456
+ code: getSampleRequest2(endpoint),
457
+ language: "js"
458
+ })
459
+ ]
460
+ )
407
461
  );
408
462
  example.push(await getResponseTabs(endpoint, method, ctx));
409
463
  return ctx.renderer.API([
@@ -411,6 +465,73 @@ async function renderOperation(path, method, ctx) {
411
465
  ctx.renderer.APIExample(example)
412
466
  ]);
413
467
  }
468
+ function getAuthSection(requirements, { document, renderer }) {
469
+ const info = [];
470
+ const schemas = document.components?.securitySchemes ?? {};
471
+ for (const requirement of requirements) {
472
+ if (info.length > 0) info.push(`---`);
473
+ for (const [name, scopes] of Object.entries(requirement)) {
474
+ if (!(name in schemas)) continue;
475
+ const schema = noRef(schemas[name]);
476
+ if (schema.type === "http") {
477
+ info.push(
478
+ renderer.Property(
479
+ {
480
+ name: "Authorization",
481
+ type: {
482
+ basic: "Basic <token>",
483
+ bearer: "Bearer <token>"
484
+ }[schema.scheme] ?? "<token>",
485
+ required: true
486
+ },
487
+ [p(schema.description), `In: \`header\``]
488
+ )
489
+ );
490
+ }
491
+ if (schema.type === "oauth2") {
492
+ info.push(
493
+ renderer.Property(
494
+ {
495
+ name: "Authorization",
496
+ type: "Bearer <token>",
497
+ required: true
498
+ },
499
+ [
500
+ p(schema.description),
501
+ `In: \`header\``,
502
+ `Scope: \`${scopes.length > 0 ? scopes.join(", ") : "none"}\``
503
+ ]
504
+ )
505
+ );
506
+ }
507
+ if (schema.type === "apiKey") {
508
+ info.push(
509
+ renderer.Property(
510
+ {
511
+ name: schema.name,
512
+ type: "<token>",
513
+ required: true
514
+ },
515
+ [p(schema.description), `In: \`${schema.in}\``]
516
+ )
517
+ );
518
+ }
519
+ if (schema.type === "openIdConnect") {
520
+ info.push(
521
+ renderer.Property(
522
+ {
523
+ name: "OpenID Connect",
524
+ type: "<token>",
525
+ required: true
526
+ },
527
+ [p(schema.description)]
528
+ )
529
+ );
530
+ }
531
+ }
532
+ }
533
+ return info.join("\n\n");
534
+ }
414
535
  function getResponseTable(operation) {
415
536
  const table = [];
416
537
  table.push(`| Status code | Description |`);
@@ -425,7 +546,7 @@ async function getResponseTabs(endpoint, operation, { renderer }) {
425
546
  const child = [];
426
547
  for (const code of Object.keys(operation.responses)) {
427
548
  const example = getExampleResponse(endpoint, code);
428
- const ts = await getTypescript(endpoint, code);
549
+ const ts = await getTypescriptSchema(endpoint, code);
429
550
  const description = code in endpoint.responses ? endpoint.responses[code].schema.description : void 0;
430
551
  if (example && ts) {
431
552
  items.push(code);
@@ -467,6 +588,29 @@ async function generate(pathOrDocument, options = {}) {
467
588
  options
468
589
  );
469
590
  }
591
+ async function generateOperations(pathOrDocument, options = {}) {
592
+ const document = await Parser.dereference(pathOrDocument);
593
+ const routes = buildRoutes(document).get("all") ?? [];
594
+ const ctx = getContext(document, options);
595
+ return await Promise.all(
596
+ routes.flatMap((route) => {
597
+ return route.methods.map(async (method) => {
598
+ const content = renderPage(
599
+ method.summary ?? method.method,
600
+ method.description,
601
+ [await renderOperation(route.path, method, ctx, true)],
602
+ options
603
+ );
604
+ if (!method.operationId)
605
+ throw new Error("Operation ID is required for generating docs.");
606
+ return {
607
+ id: method.operationId,
608
+ content
609
+ };
610
+ });
611
+ })
612
+ );
613
+ }
470
614
  async function generateTags(pathOrDocument, options = {}) {
471
615
  const document = await Parser.dereference(pathOrDocument);
472
616
  const tags = Array.from(buildRoutes(document).entries());
@@ -489,6 +633,7 @@ async function generateTags(pathOrDocument, options = {}) {
489
633
  }
490
634
  function getContext(document, options) {
491
635
  return {
636
+ document,
492
637
  renderer: {
493
638
  ...defaultRenderer,
494
639
  ...options.renderer
@@ -522,10 +667,24 @@ async function generateFiles({
522
667
  console.log(`Generated: ${outPath}`);
523
668
  return;
524
669
  }
670
+ if (per === "operation") {
671
+ const results2 = await generateOperations(path, options);
672
+ await Promise.all(
673
+ results2.map(async (result) => {
674
+ const outPath = join(
675
+ outputDir,
676
+ filename,
677
+ `${getName(result.id)}.mdx`
678
+ );
679
+ await write(outPath, result.content);
680
+ console.log(`Generated: ${outPath}`);
681
+ })
682
+ );
683
+ }
525
684
  const results = await generateTags(path, options);
526
685
  for (const result of results) {
527
686
  let tagName = result.tag;
528
- tagName = nameFn?.("tag", tagName) ?? tagName.toLowerCase().replace(/\s+/g, "-");
687
+ tagName = nameFn?.("tag", tagName) ?? getName(tagName);
529
688
  const outPath = join(outputDir, filename, `${tagName}.mdx`);
530
689
  await write(outPath, result.content);
531
690
  console.log(`Generated: ${outPath}`);
@@ -533,6 +692,12 @@ async function generateFiles({
533
692
  })
534
693
  );
535
694
  }
695
+ function getName(s) {
696
+ return s.replace(
697
+ /[A-Z]/g,
698
+ (match, idx) => idx === 0 ? match : `-${match.toLowerCase()}`
699
+ ).replace(/\s+/g, "-").toLowerCase();
700
+ }
536
701
  async function write(path, content) {
537
702
  await mkdir(dirname(path), { recursive: true });
538
703
  await writeFile(path, content);
@@ -542,5 +707,6 @@ export {
542
707
  defaultRenderer,
543
708
  generate,
544
709
  generateFiles,
710
+ generateOperations,
545
711
  generateTags
546
712
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fumadocs-openapi",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "Generate MDX docs for your OpenAPI spec",
5
5
  "keywords": [
6
6
  "NextJs",