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.
- package/dist/index.d.ts +68 -20
- package/dist/index.js +343 -211
- 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
|
-
|
|
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
|
|
49
|
+
* The imports of your MDX components.
|
|
8
50
|
*
|
|
9
|
-
*
|
|
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
|
-
|
|
12
|
-
|
|
60
|
+
frontmatter?: (title: string, description: string | undefined) => Record<string, unknown>;
|
|
61
|
+
renderer?: Partial<Renderer>;
|
|
13
62
|
}
|
|
14
|
-
interface
|
|
15
|
-
|
|
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?:
|
|
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,
|
|
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,
|
|
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/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
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/
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
131
|
+
function toSampleInput(value) {
|
|
74
132
|
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
75
133
|
}
|
|
76
134
|
|
|
77
|
-
// src/
|
|
78
|
-
|
|
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 =
|
|
106
|
-
if (param.in === "query")
|
|
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
|
-
|
|
178
|
+
toSampleInput(value)
|
|
111
179
|
);
|
|
112
180
|
}
|
|
113
181
|
return {
|
|
114
182
|
url: new URL(pathWithParameters, baseUrl).toString(),
|
|
115
|
-
body: bodySchema ?
|
|
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/
|
|
190
|
+
// src/utils/generate-response.ts
|
|
129
191
|
function getExampleResponse(endpoint, code) {
|
|
130
192
|
if (code in endpoint.responses) {
|
|
131
|
-
const value =
|
|
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/
|
|
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 =
|
|
159
|
-
const header = `${param.name}: ${
|
|
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 '${
|
|
215
|
+
if (endpoint.body) s.push(`-d '${toSampleInput(endpoint.body)}'`);
|
|
167
216
|
return s.join(" \\\n ");
|
|
168
217
|
}
|
|
169
|
-
|
|
170
|
-
|
|
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,
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
|
543
|
+
for (const code of Object.keys(operation.responses)) {
|
|
365
544
|
const example = getExampleResponse(endpoint, code);
|
|
366
|
-
const ts = await
|
|
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
|
-
|
|
372
|
-
{ value: code },
|
|
550
|
+
renderer.Response({ value: code }, [
|
|
373
551
|
p(description),
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
561
|
+
return renderer.Responses(
|
|
387
562
|
{
|
|
388
563
|
items
|
|
389
564
|
},
|
|
390
|
-
|
|
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
|
|
401
|
-
const
|
|
402
|
-
|
|
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
|
-
|
|
577
|
+
child.push(await renderOperation(route.path, method, ctx));
|
|
431
578
|
}
|
|
432
579
|
}
|
|
433
|
-
return
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
|
|
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
|
|
607
|
+
function getContext(document, options) {
|
|
477
608
|
return {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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 {
|
|
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
|
-
|
|
494
|
-
|
|
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
|
-
|
|
525
|
-
|
|
654
|
+
async function write(path, content) {
|
|
655
|
+
await mkdir(dirname(path), { recursive: true });
|
|
656
|
+
await writeFile(path, content);
|
|
526
657
|
}
|
|
527
658
|
export {
|
|
528
|
-
|
|
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": "
|
|
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",
|