fumadocs-openapi 4.2.1 → 4.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.
@@ -0,0 +1,61 @@
1
+ // src/ui/contexts/api.tsx
2
+ import { createContext, useContext, useEffect, useState } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var ApiContext = createContext({
5
+ baseUrl: void 0,
6
+ setBaseUrl: () => void 0,
7
+ highlighter: null
8
+ });
9
+ function useApiContext() {
10
+ return useContext(ApiContext);
11
+ }
12
+ async function initHighlighter() {
13
+ const { createHighlighterCore } = await import("shiki/core");
14
+ const getWasm = await import("shiki/wasm");
15
+ return createHighlighterCore({
16
+ themes: [
17
+ import("shiki/themes/github-light.mjs"),
18
+ import("shiki/themes/github-dark.mjs")
19
+ ],
20
+ langs: [import("shiki/langs/json.mjs")],
21
+ loadWasm: getWasm
22
+ });
23
+ }
24
+ var highlighterInstance;
25
+ function ApiProvider({
26
+ defaultBaseUrl,
27
+ children
28
+ }) {
29
+ const [highlighter, setHighlighter] = useState(null);
30
+ const [baseUrl, setBaseUrl] = useState(defaultBaseUrl);
31
+ useEffect(() => {
32
+ setBaseUrl((prev) => localStorage.getItem("apiBaseUrl") ?? prev);
33
+ if (highlighterInstance) setHighlighter(highlighterInstance);
34
+ else
35
+ void initHighlighter().then((res) => {
36
+ setHighlighter(res);
37
+ });
38
+ }, []);
39
+ useEffect(() => {
40
+ if (baseUrl) localStorage.setItem("apiBaseUrl", baseUrl);
41
+ }, [baseUrl]);
42
+ return /* @__PURE__ */ jsx(ApiContext.Provider, { value: { baseUrl, setBaseUrl, highlighter }, children });
43
+ }
44
+
45
+ // src/ui/contexts/schema.tsx
46
+ import { createContext as createContext2, useContext as useContext2 } from "react";
47
+ var SchemaContext = createContext2(
48
+ void 0
49
+ );
50
+ function useSchemaContext() {
51
+ const ctx = useContext2(SchemaContext);
52
+ if (!ctx) throw new Error("Missing provider");
53
+ return ctx;
54
+ }
55
+
56
+ export {
57
+ useApiContext,
58
+ ApiProvider,
59
+ SchemaContext,
60
+ useSchemaContext
61
+ };
@@ -61,69 +61,10 @@ function resolve(schema, references) {
61
61
  };
62
62
  }
63
63
 
64
- // src/ui/contexts/api.tsx
65
- import { createContext, useContext, useEffect, useState } from "react";
66
- import { jsx } from "react/jsx-runtime";
67
- var ApiContext = createContext({
68
- baseUrl: void 0,
69
- setBaseUrl: () => void 0,
70
- highlighter: null
71
- });
72
- function useApiContext() {
73
- return useContext(ApiContext);
74
- }
75
- async function initHighlighter() {
76
- const { createHighlighterCore } = await import("shiki/core");
77
- const getWasm = await import("shiki/wasm");
78
- return createHighlighterCore({
79
- themes: [
80
- import("shiki/themes/github-light.mjs"),
81
- import("shiki/themes/github-dark.mjs")
82
- ],
83
- langs: [import("shiki/langs/json.mjs")],
84
- loadWasm: getWasm
85
- });
86
- }
87
- var highlighterInstance;
88
- function ApiProvider({
89
- defaultBaseUrl,
90
- children
91
- }) {
92
- const [highlighter, setHighlighter] = useState(null);
93
- const [baseUrl, setBaseUrl] = useState(defaultBaseUrl);
94
- useEffect(() => {
95
- setBaseUrl((prev) => localStorage.getItem("apiBaseUrl") ?? prev);
96
- if (highlighterInstance) setHighlighter(highlighterInstance);
97
- else
98
- void initHighlighter().then((res) => {
99
- setHighlighter(res);
100
- });
101
- }, []);
102
- useEffect(() => {
103
- if (baseUrl) localStorage.setItem("apiBaseUrl", baseUrl);
104
- }, [baseUrl]);
105
- return /* @__PURE__ */ jsx(ApiContext.Provider, { value: { baseUrl, setBaseUrl, highlighter }, children });
106
- }
107
-
108
- // src/ui/contexts/schema.tsx
109
- import { createContext as createContext2, useContext as useContext2 } from "react";
110
- var SchemaContext = createContext2(
111
- void 0
112
- );
113
- function useSchemaContext() {
114
- const ctx = useContext2(SchemaContext);
115
- if (!ctx) throw new Error("Missing provider");
116
- return ctx;
117
- }
118
-
119
64
  export {
120
65
  badgeVariants,
121
66
  getBadgeColor,
122
67
  getDefaultValue,
123
68
  getDefaultValues,
124
- resolve,
125
- useApiContext,
126
- ApiProvider,
127
- SchemaContext,
128
- useSchemaContext
69
+ resolve
129
70
  };
package/dist/index.d.ts CHANGED
@@ -106,6 +106,24 @@ interface RenderContext {
106
106
  generateCodeSamples?: (endpoint: Endpoint) => Awaitable<CodeSample[]>;
107
107
  }
108
108
 
109
+ type DocumentContext = {
110
+ type: 'tag';
111
+ tag: OpenAPIV3.TagObject | undefined;
112
+ routes: RouteInformation[];
113
+ } | {
114
+ type: 'operation';
115
+ /**
116
+ * information of the route
117
+ */
118
+ route: RouteInformation;
119
+ /**
120
+ * information of the method (API Endpoint)
121
+ */
122
+ endpoint: MethodInformation;
123
+ } | {
124
+ type: 'file';
125
+ routes: RouteInformation[];
126
+ };
109
127
  interface GenerateOptions extends Pick<RenderContext, 'generateCodeSamples' | 'generateTypeScriptSchema'> {
110
128
  /**
111
129
  * The imports of your MDX components.
@@ -121,7 +139,7 @@ interface GenerateOptions extends Pick<RenderContext, 'generateCodeSamples' | 'g
121
139
  *
122
140
  * A `full: true` property will be added by default.
123
141
  */
124
- frontmatter?: (title: string, description: string | undefined) => Record<string, unknown>;
142
+ frontmatter?: (title: string, description: string | undefined, context: DocumentContext) => Record<string, unknown>;
125
143
  renderer?: Partial<Renderer>;
126
144
  }
127
145
  interface GenerateTagOutput {
@@ -131,6 +149,7 @@ interface GenerateTagOutput {
131
149
  interface GenerateOperationOutput {
132
150
  id: string;
133
151
  content: string;
152
+ route: RouteInformation;
134
153
  }
135
154
  declare function generate(pathOrDocument: string | OpenAPIV3.Document, options?: GenerateOptions): Promise<string>;
136
155
  declare function generateOperations(pathOrDocument: string | OpenAPIV3.Document, options?: GenerateOptions): Promise<GenerateOperationOutput[]>;
@@ -157,10 +176,16 @@ interface Config extends GenerateOptions {
157
176
  * Specify name for output file
158
177
  */
159
178
  name?: (type: 'file' | 'tag', name: string) => string;
179
+ /**
180
+ * Group output using folders (Only works on `operation` mode)
181
+ *
182
+ * @defaultValue false
183
+ */
184
+ groupByFolder?: boolean;
160
185
  cwd?: string;
161
186
  }
162
- declare function generateFiles({ input, output, name: nameFn, per, cwd, ...options }: Config): Promise<void>;
187
+ declare function generateFiles({ input, output, name: nameFn, per, cwd, groupByFolder, ...options }: Config): Promise<void>;
163
188
 
164
189
  declare function createElement(name: string, props: object, ...child: string[]): string;
165
190
 
166
- export { type APIInfoProps, APIPlaygroundProps, 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 RootProps, type RouteInformation, createElement, defaultRenderer, generate, generateFiles, generateOperations, generateTags };
191
+ export { type APIInfoProps, APIPlaygroundProps, type Config, type DocumentContext, type GenerateOperationOutput, type GenerateOptions, type GenerateTagOutput, type MethodInformation, type ObjectCollapsibleProps, type PropertyProps, type RenderContext, type Renderer, type RequestProps, type ResponseProps, type ResponsesProps, type RootProps, type RouteInformation, createElement, defaultRenderer, generate, generateFiles, generateOperations, generateTags };
package/dist/index.js CHANGED
@@ -99,12 +99,20 @@ var defaultRenderer = {
99
99
  };
100
100
 
101
101
  // src/utils/generate-document.ts
102
- function generateDocument(title, description, content, options) {
102
+ function generateDocument(content, options, frontmatter) {
103
103
  const banner = dump({
104
- title,
105
- description,
104
+ title: frontmatter.title,
105
+ description: frontmatter.description,
106
106
  full: true,
107
- ...options.frontmatter?.(title, description)
107
+ ...frontmatter.context.type === "operation" ? {
108
+ method: frontmatter.context.endpoint.method,
109
+ route: frontmatter.context.route.path
110
+ } : void 0,
111
+ ...options.frontmatter?.(
112
+ frontmatter.title,
113
+ frontmatter.description,
114
+ frontmatter.context
115
+ )
108
116
  }).trim();
109
117
  const finalImports = (options.imports ?? [
110
118
  {
@@ -620,13 +628,13 @@ function getSchemaType(schema, ctx) {
620
628
  }
621
629
 
622
630
  // src/render/operation.ts
623
- async function renderOperation(path, method, ctx, noTitle = false) {
631
+ async function renderOperation(path, method, ctx, hasHead = true) {
624
632
  let level = 2;
625
633
  const body = noRef(method.requestBody);
626
634
  const security = method.security ?? ctx.document.security;
627
635
  const info = [];
628
636
  const example = [];
629
- if (!noTitle) {
637
+ if (hasHead) {
630
638
  info.push(
631
639
  heading(
632
640
  level,
@@ -634,8 +642,8 @@ async function renderOperation(path, method, ctx, noTitle = false) {
634
642
  )
635
643
  );
636
644
  level++;
645
+ if (method.description) info.push(p(method.description));
637
646
  }
638
- if (method.description) info.push(p(method.description));
639
647
  info.push(renderPlayground(path, method, ctx));
640
648
  if (security) {
641
649
  info.push(heading(level, "Authorization"));
@@ -856,10 +864,15 @@ async function generate(pathOrDocument, options = {}) {
856
864
  }
857
865
  }
858
866
  return generateDocument(
859
- document.info.title,
860
- document.info.description,
861
867
  ctx.renderer.Root({ baseUrl: ctx.baseUrl }, child),
862
- options
868
+ options,
869
+ {
870
+ ...document.info,
871
+ context: {
872
+ type: "file",
873
+ routes
874
+ }
875
+ }
863
876
  );
864
877
  }
865
878
  async function generateOperations(pathOrDocument, options = {}) {
@@ -869,19 +882,27 @@ async function generateOperations(pathOrDocument, options = {}) {
869
882
  return await Promise.all(
870
883
  routes.flatMap((route) => {
871
884
  return route.methods.map(async (method) => {
885
+ if (!method.operationId)
886
+ throw new Error("Operation ID is required for generating docs.");
872
887
  const content = generateDocument(
873
- method.summary ?? method.method,
874
- method.description,
875
888
  ctx.renderer.Root({ baseUrl: ctx.baseUrl }, [
876
- await renderOperation(route.path, method, ctx, true)
889
+ await renderOperation(route.path, method, ctx, false)
877
890
  ]),
878
- options
891
+ options,
892
+ {
893
+ title: method.summary ?? method.method,
894
+ description: method.description,
895
+ context: {
896
+ type: "operation",
897
+ endpoint: method,
898
+ route
899
+ }
900
+ }
879
901
  );
880
- if (!method.operationId)
881
- throw new Error("Operation ID is required for generating docs.");
882
902
  return {
883
903
  id: method.operationId,
884
- content
904
+ content,
905
+ route
885
906
  };
886
907
  });
887
908
  })
@@ -903,10 +924,17 @@ async function generateTags(pathOrDocument, options = {}) {
903
924
  return {
904
925
  tag,
905
926
  content: generateDocument(
906
- idToTitle(tag),
907
- info?.description,
908
927
  ctx.renderer.Root({ baseUrl: ctx.baseUrl }, child),
909
- options
928
+ options,
929
+ {
930
+ title: idToTitle(tag),
931
+ description: info?.description,
932
+ context: {
933
+ type: "tag",
934
+ tag: info,
935
+ routes
936
+ }
937
+ }
910
938
  )
911
939
  };
912
940
  })
@@ -935,6 +963,7 @@ async function generateFiles({
935
963
  name: nameFn,
936
964
  per = "file",
937
965
  cwd = process.cwd(),
966
+ groupByFolder = false,
938
967
  ...options
939
968
  }) {
940
969
  const outputDir = join(cwd, output);
@@ -951,23 +980,39 @@ async function generateFiles({
951
980
  return;
952
981
  }
953
982
  if (per === "operation") {
983
+ const routeFolders = /* @__PURE__ */ new Set();
954
984
  const results2 = await generateOperations(path, options);
955
985
  await Promise.all(
956
986
  results2.map(async (result) => {
957
- const outPath = join(
987
+ const outPath = groupByFolder ? join(
958
988
  outputDir,
959
989
  filename,
960
- `${getName(result.id)}.mdx`
961
- );
990
+ result.route.summary ? getFilename(result.route.summary) : getFilenameFromRoute(result.route.path),
991
+ `${getFilename(result.id)}.mdx`
992
+ ) : join(outputDir, filename, `${getFilename(result.id)}.mdx`);
993
+ if (groupByFolder && !routeFolders.has(dirname(outPath))) {
994
+ routeFolders.add(dirname(outPath));
995
+ if (result.route.summary) {
996
+ const metaFile = join(dirname(outPath), "meta.json");
997
+ await write(
998
+ metaFile,
999
+ JSON.stringify({
1000
+ title: result.route.summary
1001
+ })
1002
+ );
1003
+ console.log(`Generated Meta: ${metaFile}`);
1004
+ }
1005
+ }
962
1006
  await write(outPath, result.content);
963
1007
  console.log(`Generated: ${outPath}`);
964
1008
  })
965
1009
  );
1010
+ return;
966
1011
  }
967
1012
  const results = await generateTags(path, options);
968
1013
  for (const result of results) {
969
1014
  let tagName = result.tag;
970
- tagName = nameFn?.("tag", tagName) ?? getName(tagName);
1015
+ tagName = nameFn?.("tag", tagName) ?? getFilename(tagName);
971
1016
  const outPath = join(outputDir, filename, `${tagName}.mdx`);
972
1017
  await write(outPath, result.content);
973
1018
  console.log(`Generated: ${outPath}`);
@@ -975,7 +1020,10 @@ async function generateFiles({
975
1020
  })
976
1021
  );
977
1022
  }
978
- function getName(s) {
1023
+ function getFilenameFromRoute(path) {
1024
+ return path.split("/").filter((v) => !v.startsWith("{") && !v.endsWith("}")).at(-1) ?? "";
1025
+ }
1026
+ function getFilename(s) {
979
1027
  return s.replace(
980
1028
  /[A-Z]/g,
981
1029
  (match, idx) => idx === 0 ? match : `-${match.toLowerCase()}`
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  SchemaContext,
3
- getDefaultValue,
4
- getDefaultValues,
5
- resolve,
6
3
  useApiContext,
7
4
  useSchemaContext
8
- } from "./chunk-55ZG37EE.js";
5
+ } from "./chunk-N4P4W4VJ.js";
6
+ import {
7
+ getDefaultValue,
8
+ getDefaultValues,
9
+ resolve
10
+ } from "./chunk-UG2WFM5D.js";
9
11
 
10
12
  // src/ui/playground.tsx
11
13
  import {
@@ -0,0 +1,10 @@
1
+ import { BuildPageTreeOptions } from 'fumadocs-core/source';
2
+
3
+ /**
4
+ * Source API Integration
5
+ *
6
+ * Add this to page tree builder options
7
+ */
8
+ declare const attachFile: BuildPageTreeOptions['attachFile'];
9
+
10
+ export { attachFile };
@@ -0,0 +1,39 @@
1
+ import {
2
+ getBadgeColor
3
+ } from "../chunk-UG2WFM5D.js";
4
+
5
+ // src/source/index.ts
6
+ import React from "react";
7
+ import { cva } from "class-variance-authority";
8
+ var attachFile = (node, file) => {
9
+ if (!file) return node;
10
+ const data = file.data.data;
11
+ if ("method" in data && typeof data.method === "string") {
12
+ node.name = [node.name, " ", ApiIndicator({ method: data.method })];
13
+ }
14
+ return node;
15
+ };
16
+ var badgeVariants = cva("rounded-full border px-1.5 text-xs font-medium", {
17
+ variants: {
18
+ color: {
19
+ green: "bg-green-400/20 text-green-600 dark:text-green-400",
20
+ yellow: "bg-yellow-400/20 text-yellow-600 dark:text-yellow-400",
21
+ red: "bg-red-400/20 text-red-600 dark:text-red-400",
22
+ blue: "bg-blue-400/20 text-blue-600 dark:text-blue-400",
23
+ orange: "bg-orange-400/20 text-orange-600 dark:text-orange-400"
24
+ }
25
+ }
26
+ });
27
+ function ApiIndicator({ method }) {
28
+ const color = getBadgeColor(method);
29
+ return React.createElement(
30
+ "span",
31
+ {
32
+ className: badgeVariants({ className: "ms-auto", color })
33
+ },
34
+ method
35
+ );
36
+ }
37
+ export {
38
+ attachFile
39
+ };
package/dist/ui/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  "use client";
2
2
  import {
3
3
  ApiProvider,
4
- badgeVariants,
5
- getBadgeColor,
6
4
  useApiContext,
7
5
  useSchemaContext
8
- } from "../chunk-55ZG37EE.js";
6
+ } from "../chunk-N4P4W4VJ.js";
7
+ import {
8
+ badgeVariants,
9
+ getBadgeColor
10
+ } from "../chunk-UG2WFM5D.js";
9
11
 
10
12
  // src/ui/index.ts
11
13
  import { Tab, Tabs } from "fumadocs-ui/components/tabs";
@@ -174,7 +176,7 @@ function CopyRouteButton({
174
176
 
175
177
  // src/ui/index.ts
176
178
  var APIPlayground = dynamic(
177
- () => import("../playground-TN274MLB.js").then((mod) => mod.APIPlayground)
179
+ () => import("../playground-5XDMCOXX.js").then((mod) => mod.APIPlayground)
178
180
  );
179
181
  var Responses = Tabs;
180
182
  var Response = Tab;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fumadocs-openapi",
3
- "version": "4.2.1",
3
+ "version": "4.3.0",
4
4
  "description": "Generate MDX docs for your OpenAPI spec",
5
5
  "keywords": [
6
6
  "NextJs",
@@ -19,6 +19,10 @@
19
19
  "./ui": {
20
20
  "import": "./dist/ui/index.js",
21
21
  "types": "./dist/ui/index.d.ts"
22
+ },
23
+ "./source": {
24
+ "import": "./dist/source/index.js",
25
+ "types": "./dist/source/index.d.ts"
22
26
  }
23
27
  },
24
28
  "main": "./dist/index.js",
@@ -30,6 +34,9 @@
30
34
  ],
31
35
  "ui": [
32
36
  "./dist/ui/index.d.ts"
37
+ ],
38
+ "source": [
39
+ "./dist/source/index.d.ts"
33
40
  ]
34
41
  }
35
42
  },
@@ -49,8 +56,8 @@
49
56
  "react-hook-form": "^7.52.1",
50
57
  "shiki": "^1.11.1",
51
58
  "swr": "^2.2.5",
52
- "fumadocs-core": "13.0.2",
53
- "fumadocs-ui": "13.0.2"
59
+ "fumadocs-core": "13.0.4",
60
+ "fumadocs-ui": "13.0.4"
54
61
  },
55
62
  "devDependencies": {
56
63
  "@types/js-yaml": "^4.0.9",