fumadocs-openapi 3.1.3 → 3.3.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
@@ -44,6 +44,48 @@ interface Renderer {
44
44
  }
45
45
  declare const defaultRenderer: Renderer;
46
46
 
47
+ interface CodeSample {
48
+ lang: string;
49
+ label: string;
50
+ source: string;
51
+ }
52
+
53
+ interface RouteInformation {
54
+ path: string;
55
+ summary?: string;
56
+ description?: string;
57
+ methods: MethodInformation[];
58
+ }
59
+ interface MethodInformation extends OpenAPIV3.OperationObject {
60
+ parameters: OpenAPIV3.ParameterObject[];
61
+ method: string;
62
+ }
63
+ interface RenderContext {
64
+ renderer: Renderer;
65
+ document: OpenAPIV3.Document;
66
+ baseUrl: string;
67
+ generateCodeSamples?: (endpoint: Endpoint) => CodeSample[];
68
+ }
69
+
70
+ interface Endpoint {
71
+ /**
72
+ * URL, including path and query parameters
73
+ */
74
+ url: string;
75
+ method: string;
76
+ body?: unknown;
77
+ responses: Record<string, Response>;
78
+ parameters: Parameter[];
79
+ }
80
+ interface Response {
81
+ schema: OpenAPIV3.SchemaObject;
82
+ }
83
+ interface Parameter {
84
+ name: string;
85
+ in: string;
86
+ schema: OpenAPIV3.SchemaObject;
87
+ }
88
+
47
89
  interface GenerateOptions {
48
90
  /**
49
91
  * The imports of your MDX components.
@@ -60,6 +102,10 @@ interface GenerateOptions {
60
102
  * A `full: true` property will be added by default.
61
103
  */
62
104
  frontmatter?: (title: string, description: string | undefined) => Record<string, unknown>;
105
+ /**
106
+ * Generate code samples for endpoint
107
+ */
108
+ generateCodeSamples?: (endpoint: Endpoint) => CodeSample[];
63
109
  renderer?: Partial<Renderer>;
64
110
  }
65
111
  interface GenerateTagOutput {
@@ -99,22 +145,6 @@ interface Config extends GenerateOptions {
99
145
  }
100
146
  declare function generateFiles({ input, output, name: nameFn, per, cwd, ...options }: Config): Promise<void>;
101
147
 
102
- interface RouteInformation {
103
- path: string;
104
- summary?: string;
105
- description?: string;
106
- methods: MethodInformation[];
107
- }
108
- interface MethodInformation extends OpenAPIV3.OperationObject {
109
- parameters: OpenAPIV3.ParameterObject[];
110
- method: string;
111
- }
112
- interface RenderContext {
113
- renderer: Renderer;
114
- document: OpenAPIV3.Document;
115
- baseUrl: string;
116
- }
117
-
118
148
  declare function createElement(name: string, props: object, ...child: string[]): string;
119
149
 
120
150
  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
@@ -281,39 +281,43 @@ function isObject(schema) {
281
281
  return schema.type === "object" || schema.properties !== void 0;
282
282
  }
283
283
  function schemaElement(name, schema, ctx) {
284
+ return render(name, schema, {
285
+ ...ctx,
286
+ stack: []
287
+ });
288
+ }
289
+ function render(name, schema, ctx) {
284
290
  if (schema.readOnly && !ctx.readOnly) return "";
285
291
  if (schema.writeOnly && !ctx.writeOnly) return "";
286
292
  const { renderer } = ctx.render;
287
293
  const child = [];
288
294
  function field(key, value) {
289
- child.push(span(`${key}: \`${value}\``));
295
+ child.push(span(`${key}: \`${value}\``));
290
296
  }
291
297
  if (isObject(schema) && ctx.parseObject) {
292
298
  const { additionalProperties, properties } = schema;
293
- if (additionalProperties) {
294
- if (additionalProperties === true) {
295
- child.push(
296
- renderer.Property(
297
- {
298
- name: "[key: string]",
299
- type: "any"
300
- },
301
- []
302
- )
303
- );
304
- } else {
305
- child.push(
306
- schemaElement("[key: string]", noRef(additionalProperties), {
307
- ...ctx,
308
- required: false,
309
- parseObject: false
310
- })
311
- );
312
- }
299
+ if (additionalProperties === true) {
300
+ child.push(
301
+ renderer.Property(
302
+ {
303
+ name: "[key: string]",
304
+ type: "any"
305
+ },
306
+ []
307
+ )
308
+ );
309
+ } else if (additionalProperties) {
310
+ child.push(
311
+ render("[key: string]", noRef(additionalProperties), {
312
+ ...ctx,
313
+ required: false,
314
+ parseObject: false
315
+ })
316
+ );
313
317
  }
314
318
  Object.entries(properties ?? {}).forEach(([key, value]) => {
315
319
  child.push(
316
- schemaElement(key, noRef(value), {
320
+ render(key, noRef(value), {
317
321
  ...ctx,
318
322
  required: schema.required?.includes(key) ?? false,
319
323
  parseObject: false
@@ -334,17 +338,38 @@ function schemaElement(name, schema, ctx) {
334
338
  schema.enum.map((value) => JSON.stringify(value)).join(" | ")
335
339
  );
336
340
  }
337
- const resolved = resolveObjectType(schema);
338
- if (resolved && !ctx.parseObject) {
341
+ if (isObject(schema) && !ctx.parseObject) {
339
342
  child.push(
340
343
  renderer.ObjectCollapsible({ name }, [
341
- schemaElement(name, resolved, {
344
+ render(name, schema, {
342
345
  ...ctx,
343
346
  parseObject: true,
344
347
  required: false
345
348
  })
346
349
  ])
347
350
  );
351
+ } else {
352
+ const mentionedObjectTypes = [
353
+ ...schema.anyOf ?? schema.oneOf ?? schema.allOf ?? [],
354
+ ...schema.not ? [schema.not] : [],
355
+ ...schema.type === "array" ? [schema.items] : []
356
+ ].map(noRef).filter((s) => isComplexType(s) && !ctx.stack.includes(s));
357
+ ctx.stack.push(schema);
358
+ child.push(
359
+ ...mentionedObjectTypes.map(
360
+ (s, idx) => renderer.ObjectCollapsible(
361
+ { name: s.title ?? `Object ${(idx + 1).toString()}` },
362
+ [
363
+ render("element", noRef(s), {
364
+ ...ctx,
365
+ parseObject: true,
366
+ required: false
367
+ })
368
+ ]
369
+ )
370
+ )
371
+ );
372
+ ctx.stack.pop();
348
373
  }
349
374
  return renderer.Property(
350
375
  {
@@ -356,28 +381,29 @@ function schemaElement(name, schema, ctx) {
356
381
  child
357
382
  );
358
383
  }
359
- function resolveObjectType(schema) {
360
- if (isObject(schema)) return schema;
361
- if (schema.type === "array") {
362
- return resolveObjectType(noRef(schema.items));
363
- }
384
+ function isComplexType(schema) {
385
+ if (schema.anyOf ?? schema.oneOf ?? schema.allOf) return true;
386
+ return isObject(schema) || schema.type === "array";
364
387
  }
365
388
  function getSchemaType(schema) {
366
389
  if (schema.nullable) {
367
- if (!schema.type) return "null";
368
- return `${getSchemaType({ ...schema, nullable: false })} | null`;
390
+ const type = getSchemaType({ ...schema, nullable: false });
391
+ return type === "unknown" ? "null" : `${type} | null`;
369
392
  }
393
+ if (schema.title) return schema.title;
370
394
  if (schema.type === "array")
371
- return `array of ${getSchemaType(noRef(schema.items))}`;
395
+ return `array<${getSchemaType(noRef(schema.items))}>`;
372
396
  if (schema.oneOf)
373
397
  return schema.oneOf.map((one) => getSchemaType(noRef(one))).join(" | ");
374
398
  if (schema.allOf)
375
399
  return schema.allOf.map((one) => getSchemaType(noRef(one))).join(" & ");
376
- if (schema.anyOf)
400
+ if (schema.not) return `not ${getSchemaType(noRef(schema.not))}`;
401
+ if (schema.anyOf) {
377
402
  return `Any properties in ${schema.anyOf.map((one) => getSchemaType(noRef(one))).join(", ")}`;
403
+ }
378
404
  if (schema.type) return schema.type;
379
405
  if (isObject(schema)) return "object";
380
- throw new Error(`Cannot detect object type: ${JSON.stringify(schema)}`);
406
+ return "unknown";
381
407
  }
382
408
 
383
409
  // src/render/operation.ts
@@ -448,21 +474,30 @@ async function renderOperation(path, method, ctx, noTitle = false) {
448
474
  info.push(heading(level, group), ...parameters);
449
475
  }
450
476
  info.push(getResponseTable(method));
477
+ const samples = dedupe([
478
+ {
479
+ label: "cURL",
480
+ source: getSampleRequest(endpoint),
481
+ lang: "bash"
482
+ },
483
+ {
484
+ label: "JavaScript",
485
+ source: getSampleRequest2(endpoint),
486
+ lang: "js"
487
+ },
488
+ ...ctx.generateCodeSamples?.(endpoint) ?? [],
489
+ ...method["x-codeSamples"] ?? []
490
+ ]);
451
491
  example.push(
452
492
  ctx.renderer.Requests(
453
- ["cURL", "JavaScript"],
454
- [
455
- ctx.renderer.Request({
456
- name: "cURL",
457
- code: getSampleRequest(endpoint),
458
- language: "bash"
459
- }),
460
- ctx.renderer.Request({
461
- name: "JavaScript",
462
- code: getSampleRequest2(endpoint),
463
- language: "js"
493
+ samples.map((s) => s.label),
494
+ samples.map(
495
+ (s) => ctx.renderer.Request({
496
+ name: s.label,
497
+ code: s.source,
498
+ language: s.lang
464
499
  })
465
- ]
500
+ )
466
501
  )
467
502
  );
468
503
  example.push(await getResponseTabs(endpoint, method, ctx));
@@ -471,6 +506,16 @@ async function renderOperation(path, method, ctx, noTitle = false) {
471
506
  ctx.renderer.APIExample(example)
472
507
  ]);
473
508
  }
509
+ function dedupe(samples) {
510
+ const set = /* @__PURE__ */ new Set();
511
+ const out = [];
512
+ for (let i = samples.length - 1; i >= 0; i--) {
513
+ if (set.has(samples[i].label)) continue;
514
+ set.add(samples[i].label);
515
+ out.unshift(samples[i]);
516
+ }
517
+ return out;
518
+ }
474
519
  function getAuthSection(requirements, { document, renderer }) {
475
520
  const info = [];
476
521
  const schemas = document.components?.securitySchemes ?? {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fumadocs-openapi",
3
- "version": "3.1.3",
3
+ "version": "3.3.0",
4
4
  "description": "Generate MDX docs for your OpenAPI spec",
5
5
  "keywords": [
6
6
  "NextJs",
@@ -20,12 +20,12 @@
20
20
  "@apidevtools/json-schema-ref-parser": "^11.6.4",
21
21
  "fast-glob": "^3.3.1",
22
22
  "js-yaml": "^4.1.0",
23
- "json-schema-to-typescript": "^14.0.5",
23
+ "json-schema-to-typescript": "^14.1.0",
24
24
  "openapi-sampler": "^1.5.1"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/js-yaml": "^4.0.9",
28
- "@types/node": "18.17.5",
28
+ "@types/node": "20.14.9",
29
29
  "@types/openapi-sampler": "^1.0.3",
30
30
  "openapi-types": "^12.1.3",
31
31
  "eslint-config-custom": "0.0.0",