fumadocs-openapi 2.0.5 → 3.0.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 +60 -20
  2. package/dist/index.js +185 -171
  3. package/package.json +3 -1
package/dist/index.d.ts CHANGED
@@ -1,28 +1,66 @@
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 Renderer {
23
+ Root: (child: string[]) => string;
24
+ API: (child: string[]) => string;
25
+ APIInfo: (props: APIInfoProps, child: string[]) => string;
26
+ APIExample: (child: string[]) => string;
27
+ Responses: (props: ResponsesProps, child: string[]) => string;
28
+ Response: (props: ResponseProps, child: string[]) => string;
29
+ ResponseTypes: (child: string[]) => string;
30
+ ExampleResponse: (json: string) => string;
31
+ TypeScriptResponse: (code: string) => string;
32
+ /**
33
+ * Collapsible to show object schemas
34
+ */
35
+ ObjectCollapsible: (props: ObjectCollapsibleProps, child: string[]) => string;
36
+ Property: (props: PropertyProps, child: string[]) => string;
37
+ }
38
+ declare const defaultRenderer: Renderer;
39
+
4
40
  interface GenerateOptions {
5
- tag?: string;
6
41
  /**
7
- * The import path of your API components, it must exports all components in `fumadocs-ui/components/api`
42
+ * The imports of your MDX components.
8
43
  *
9
- * @defaultValue `fumadocs-ui/components/api`
44
+ * If not specified, import required components from `fumadocs-ui/components/api`.
10
45
  */
11
- componentsImportPath?: string;
12
- render?: (title: string | undefined, description: string | undefined, content: string) => Partial<RenderResult>;
46
+ imports?: {
47
+ names: string[];
48
+ from: string;
49
+ }[];
50
+ /**
51
+ * Customise frontmatter
52
+ */
53
+ frontmatter?: (title: string, description: string | undefined) => Record<string, unknown>;
54
+ renderer?: Partial<Renderer>;
13
55
  }
14
- interface RenderResult {
15
- frontmatter: string;
16
- imports: string[];
56
+ interface GenerateTagOutput {
57
+ tag: string;
17
58
  content: string;
18
59
  }
19
60
  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
- }[]>;
61
+ declare function generateTags(pathOrDocument: string | OpenAPIV3.Document, options?: GenerateOptions): Promise<GenerateTagOutput[]>;
24
62
 
25
- interface Config {
63
+ interface Config extends GenerateOptions {
26
64
  /**
27
65
  * Schema files
28
66
  */
@@ -43,13 +81,9 @@ interface Config {
43
81
  * Specify name for output file
44
82
  */
45
83
  name?: (type: 'file' | 'tag', name: string) => string;
46
- /**
47
- * Modify output file
48
- */
49
- render?: NonNullable<GenerateOptions['render']>;
50
84
  cwd?: string;
51
85
  }
52
- declare function generateFiles({ input, output, name: nameFn, per, render, cwd, }: Config): Promise<void>;
86
+ declare function generateFiles({ input, output, name: nameFn, per, cwd, ...options }: Config): Promise<void>;
53
87
 
54
88
  interface RouteInformation {
55
89
  path: string;
@@ -61,5 +95,11 @@ interface MethodInformation extends OpenAPIV3.OperationObject {
61
95
  parameters: OpenAPIV3.ParameterObject[];
62
96
  method: string;
63
97
  }
98
+ interface RenderContext {
99
+ renderer: Renderer;
100
+ baseUrl: string;
101
+ }
102
+
103
+ declare function createElement(name: string, props: object, ...child: string[]): string;
64
104
 
65
- export { type Config, type GenerateOptions, type MethodInformation, type RouteInformation, dereference, generate, generateFiles, generateTags };
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 };
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,42 +68,53 @@ 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
- );
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
+ };
94
+
95
+ // src/render/page.ts
96
+ function renderPage(title, description, content, options) {
97
+ const banner = dump({
98
+ title,
99
+ description,
100
+ ...options.frontmatter?.(title, description)
101
+ }).trim();
102
+ const finalImports = (options.imports ?? [
103
+ {
104
+ names: Object.keys(defaultRenderer),
105
+ from: "fumadocs-ui/components/api"
106
+ }
107
+ ]).map(
108
+ (item) => `import { ${item.names.join(", ")} } from ${JSON.stringify(item.from)};`
109
+ ).join("\n");
110
+ const Root = options.renderer?.Root ?? defaultRenderer.Root;
111
+ return `---
112
+ ${banner}
113
+ ---
114
+
115
+ ${finalImports}
116
+
117
+ ${Root(content)}`;
59
118
  }
60
119
 
61
120
  // src/samples/index.ts
@@ -75,7 +134,7 @@ function getValue(value) {
75
134
  }
76
135
 
77
136
  // src/samples/index.ts
78
- function createEndpoint(path, method, baseUrl = "https://example.com") {
137
+ function createEndpoint(path, method, baseUrl) {
79
138
  const params = [];
80
139
  const responses = {};
81
140
  for (const param of method.parameters) {
@@ -184,22 +243,26 @@ var keys = {
184
243
  function isObject(schema) {
185
244
  return schema.type === "object" || schema.properties !== void 0;
186
245
  }
187
- function schemaElement(name, schema, { parseObject, ...ctx }) {
246
+ function schemaElement(name, schema, ctx) {
188
247
  if (schema.readOnly && !ctx.readOnly) return "";
189
248
  if (schema.writeOnly && !ctx.writeOnly) return "";
249
+ const { renderer } = ctx.render;
190
250
  const child = [];
191
251
  function field(key, value) {
192
252
  child.push(span(`${key}: \`${value}\``));
193
253
  }
194
- if (isObject(schema) && parseObject) {
254
+ if (isObject(schema) && ctx.parseObject) {
195
255
  const { additionalProperties, properties } = schema;
196
256
  if (additionalProperties) {
197
257
  if (additionalProperties === true) {
198
258
  child.push(
199
- property({
200
- name: "[key: string]",
201
- type: "any"
202
- })
259
+ renderer.Property(
260
+ {
261
+ name: "[key: string]",
262
+ type: "any"
263
+ },
264
+ []
265
+ )
203
266
  );
204
267
  } else {
205
268
  child.push(
@@ -235,28 +298,25 @@ function schemaElement(name, schema, { parseObject, ...ctx }) {
235
298
  );
236
299
  }
237
300
  const resolved = resolveObjectType(schema);
238
- if (resolved && !parseObject) {
301
+ if (resolved && !ctx.parseObject) {
239
302
  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
- )
303
+ renderer.ObjectCollapsible({ name }, [
304
+ schemaElement(name, resolved, {
305
+ ...ctx,
306
+ parseObject: true,
307
+ required: false
308
+ })
309
+ ])
250
310
  );
251
311
  }
252
- return property(
312
+ return renderer.Property(
253
313
  {
254
314
  name,
255
315
  type: getSchemaType(schema),
256
316
  required: ctx.required,
257
317
  deprecated: schema.deprecated
258
318
  },
259
- ...child
319
+ child
260
320
  );
261
321
  }
262
322
  function resolveObjectType(schema) {
@@ -284,7 +344,7 @@ function getSchemaType(schema) {
284
344
  }
285
345
 
286
346
  // src/render/operation.ts
287
- async function renderOperation(path, method, baseUrl) {
347
+ async function renderOperation(path, method, ctx) {
288
348
  const info = [];
289
349
  const example = [];
290
350
  const title = method.summary ?? method.operationId;
@@ -301,12 +361,13 @@ async function renderOperation(path, method, baseUrl) {
301
361
  parseObject: true,
302
362
  readOnly: method.method === "GET",
303
363
  writeOnly: method.method !== "GET",
304
- required: body.required ?? false
364
+ required: body.required ?? false,
365
+ render: ctx
305
366
  })
306
367
  );
307
368
  }
308
369
  const parameterGroups = /* @__PURE__ */ new Map();
309
- const endpoint = createEndpoint(path, method, baseUrl);
370
+ const endpoint = createEndpoint(path, method, ctx.baseUrl);
310
371
  for (const param of method.parameters) {
311
372
  const schema = noRef(
312
373
  param.schema ?? getPreferredMedia(param.content ?? {})?.schema
@@ -323,7 +384,8 @@ async function renderOperation(path, method, baseUrl) {
323
384
  parseObject: false,
324
385
  readOnly: method.method === "GET",
325
386
  writeOnly: method.method !== "GET",
326
- required: param.required ?? false
387
+ required: param.required ?? false,
388
+ render: ctx
327
389
  }
328
390
  );
329
391
  const groupName = {
@@ -343,11 +405,11 @@ async function renderOperation(path, method, baseUrl) {
343
405
  example.push(
344
406
  codeblock({ language: "bash", title: "curl" }, getSampleRequest(endpoint))
345
407
  );
346
- example.push(await getResponseTabs(endpoint, method));
347
- return api(
348
- apiInfo({ method: method.method, route: path }, ...info),
349
- apiExample(...example)
350
- );
408
+ example.push(await getResponseTabs(endpoint, method, ctx));
409
+ return ctx.renderer.API([
410
+ ctx.renderer.APIInfo({ method: method.method, route: path }, info),
411
+ ctx.renderer.APIExample(example)
412
+ ]);
351
413
  }
352
414
  function getResponseTable(operation) {
353
415
  const table = [];
@@ -358,131 +420,85 @@ function getResponseTable(operation) {
358
420
  });
359
421
  return table.join("\n");
360
422
  }
361
- async function getResponseTabs(endpoint, operation) {
423
+ async function getResponseTabs(endpoint, operation, { renderer }) {
362
424
  const items = [];
363
425
  const child = [];
364
- for (const [code, _] of Object.entries(operation.responses)) {
426
+ for (const code of Object.keys(operation.responses)) {
365
427
  const example = getExampleResponse(endpoint, code);
366
428
  const ts = await getTypescript(endpoint, code);
367
429
  const description = code in endpoint.responses ? endpoint.responses[code].schema.description : void 0;
368
430
  if (example && ts) {
369
431
  items.push(code);
370
432
  child.push(
371
- tab(
372
- { value: code },
433
+ renderer.Response({ value: code }, [
373
434
  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
- )
435
+ renderer.ResponseTypes([
436
+ renderer.ExampleResponse(example),
437
+ renderer.TypeScriptResponse(ts)
438
+ ])
439
+ ])
382
440
  );
383
441
  }
384
442
  }
385
443
  if (items.length === 0) return "";
386
- return tabs(
444
+ return renderer.Responses(
387
445
  {
388
446
  items
389
447
  },
390
- ...child
448
+ child
391
449
  );
392
450
  }
393
451
 
394
452
  // src/generate.ts
395
- async function dereference(pathOrDocument) {
396
- return await Parser.dereference(pathOrDocument);
397
- }
398
453
  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 = [];
454
+ const document = await Parser.dereference(pathOrDocument);
455
+ const routes = buildRoutes(document).get("all") ?? [];
456
+ const ctx = getContext(document, options);
457
+ const child = [];
428
458
  for (const route of routes) {
429
459
  for (const method of route.methods) {
430
- s.push(await renderOperation(route.path, method, serverUrl));
460
+ child.push(await renderOperation(route.path, method, ctx));
431
461
  }
432
462
  }
433
- return render(
434
- tag?.name ?? document.info.title,
435
- tag?.description ?? document.info.description,
436
- root(...s),
463
+ return renderPage(
464
+ document.info.title,
465
+ document.info.description,
466
+ child,
437
467
  options
438
468
  );
439
469
  }
440
470
  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");
471
+ const document = await Parser.dereference(pathOrDocument);
472
+ const tags = Array.from(buildRoutes(document).entries());
473
+ const ctx = getContext(document, options);
474
+ return await Promise.all(
475
+ tags.filter(([tag]) => tag !== "all").map(async ([tag, routes]) => {
476
+ const info = document.tags?.find((t) => t.name === tag);
477
+ const child = [];
478
+ for (const route of routes) {
479
+ for (const method of route.methods) {
480
+ child.push(await renderOperation(route.path, method, ctx));
481
+ }
482
+ }
483
+ return {
484
+ tag,
485
+ content: renderPage(tag, info?.description, child, options)
486
+ };
487
+ })
488
+ );
475
489
  }
476
- function buildOperation(method, operation) {
490
+ function getContext(document, options) {
477
491
  return {
478
- ...operation,
479
- parameters: operation.parameters ?? [],
480
- method: method.toUpperCase()
492
+ renderer: {
493
+ ...defaultRenderer,
494
+ ...options.renderer
495
+ },
496
+ baseUrl: document.servers?.[0].url ?? "https://example.com"
481
497
  };
482
498
  }
483
499
 
484
500
  // src/generate-file.ts
485
- import { mkdirSync, writeFileSync } from "node:fs";
501
+ import { mkdir, writeFile } from "node:fs/promises";
486
502
  import { dirname, join, parse } from "node:path";
487
503
  import fg from "fast-glob";
488
504
  async function generateFiles({
@@ -490,13 +506,10 @@ async function generateFiles({
490
506
  output,
491
507
  name: nameFn,
492
508
  per = "file",
493
- render: render2,
494
- cwd = process.cwd()
509
+ cwd = process.cwd(),
510
+ ...options
495
511
  }) {
496
512
  const outputDir = join(cwd, output);
497
- const options = {
498
- render: render2
499
- };
500
513
  const resolvedInputs = await fg.glob(input, { absolute: true, cwd });
501
514
  await Promise.all(
502
515
  resolvedInputs.map(async (path) => {
@@ -505,27 +518,28 @@ async function generateFiles({
505
518
  if (per === "file") {
506
519
  const outPath = join(outputDir, `${filename}.mdx`);
507
520
  const result = await generate(path, options);
508
- write(outPath, result);
521
+ await write(outPath, result);
522
+ console.log(`Generated: ${outPath}`);
523
+ return;
524
+ }
525
+ const results = await generateTags(path, options);
526
+ for (const result of results) {
527
+ let tagName = result.tag;
528
+ tagName = nameFn?.("tag", tagName) ?? tagName.toLowerCase().replace(/\s+/g, "-");
529
+ const outPath = join(outputDir, filename, `${tagName}.mdx`);
530
+ await write(outPath, result.content);
509
531
  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
532
  }
520
533
  })
521
534
  );
522
535
  }
523
- function write(path, content) {
524
- mkdirSync(dirname(path), { recursive: true });
525
- writeFileSync(path, content);
536
+ async function write(path, content) {
537
+ await mkdir(dirname(path), { recursive: true });
538
+ await writeFile(path, content);
526
539
  }
527
540
  export {
528
- dereference,
541
+ createElement,
542
+ defaultRenderer,
529
543
  generate,
530
544
  generateFiles,
531
545
  generateTags
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fumadocs-openapi",
3
- "version": "2.0.5",
3
+ "version": "3.0.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",