fumadocs-openapi 1.0.1 → 2.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.
- package/dist/index.d.ts +3 -2
- package/dist/index.js +534 -6
- package/package.json +2 -4
- package/dist/bin.d.ts +0 -1
- package/dist/bin.js +0 -21
- package/dist/chunk-6NCGY6WV.js +0 -539
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ interface Config {
|
|
|
26
26
|
/**
|
|
27
27
|
* Schema files
|
|
28
28
|
*/
|
|
29
|
-
input: string[];
|
|
29
|
+
input: string[] | string;
|
|
30
30
|
/**
|
|
31
31
|
* Output directory
|
|
32
32
|
*/
|
|
@@ -47,8 +47,9 @@ interface Config {
|
|
|
47
47
|
* Modify output file
|
|
48
48
|
*/
|
|
49
49
|
render?: NonNullable<GenerateOptions['render']>;
|
|
50
|
+
cwd?: string;
|
|
50
51
|
}
|
|
51
|
-
declare function generateFiles({ input, output, name: nameFn, per, render, }: Config): Promise<void>;
|
|
52
|
+
declare function generateFiles({ input, output, name: nameFn, per, render, cwd, }: Config): Promise<void>;
|
|
52
53
|
|
|
53
54
|
interface RouteInformation {
|
|
54
55
|
path: string;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,537 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
// src/generate.ts
|
|
2
|
+
import Parser from "@apidevtools/swagger-parser";
|
|
3
|
+
|
|
4
|
+
// src/render/element.ts
|
|
5
|
+
function createElement(name, props, ...child) {
|
|
6
|
+
const s = [];
|
|
7
|
+
const params = Object.entries(props).map(([key, value]) => `${key}={${JSON.stringify(value)}}`).join(" ");
|
|
8
|
+
s.push(params.length > 0 ? `<${name} ${params}>` : `<${name}>`);
|
|
9
|
+
s.push(...child);
|
|
10
|
+
s.push(`</${name}>`);
|
|
11
|
+
return s.join("\n\n");
|
|
12
|
+
}
|
|
13
|
+
function p(child) {
|
|
14
|
+
if (!child)
|
|
15
|
+
return "";
|
|
16
|
+
return child.replace("<", "\\<").replace(">", "\\>");
|
|
17
|
+
}
|
|
18
|
+
function span(child) {
|
|
19
|
+
return `<span>${p(child)}</span>`;
|
|
20
|
+
}
|
|
21
|
+
function codeblock({ language, title }, child) {
|
|
22
|
+
return [
|
|
23
|
+
title ? `\`\`\`${language} title=${JSON.stringify(title)}` : `\`\`\`${language}`,
|
|
24
|
+
child,
|
|
25
|
+
"```"
|
|
26
|
+
].join("\n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/render/custom.ts
|
|
30
|
+
function api(...child) {
|
|
31
|
+
return createElement("API", {}, ...child);
|
|
32
|
+
}
|
|
33
|
+
function apiExample(...child) {
|
|
34
|
+
return createElement("APIExample", {}, ...child);
|
|
35
|
+
}
|
|
36
|
+
function root(...child) {
|
|
37
|
+
return createElement("Root", {}, ...child);
|
|
38
|
+
}
|
|
39
|
+
function apiInfo(props, ...child) {
|
|
40
|
+
return createElement("APIInfo", props, ...child);
|
|
41
|
+
}
|
|
42
|
+
function accordions(...child) {
|
|
43
|
+
return createElement("Accordions", {}, ...child);
|
|
44
|
+
}
|
|
45
|
+
function accordion(props, ...child) {
|
|
46
|
+
return createElement("Accordion", props, ...child);
|
|
47
|
+
}
|
|
48
|
+
function tabs(props, ...child) {
|
|
49
|
+
return createElement("Tabs", props, ...child);
|
|
50
|
+
}
|
|
51
|
+
function tab(props, ...child) {
|
|
52
|
+
return createElement("Tab", props, ...child);
|
|
53
|
+
}
|
|
54
|
+
function property({ required = false, deprecated = false, ...props }, ...child) {
|
|
55
|
+
return createElement(
|
|
56
|
+
"Property",
|
|
57
|
+
{ required, deprecated, ...props },
|
|
58
|
+
...child
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/samples/index.ts
|
|
63
|
+
import { sample } from "openapi-sampler";
|
|
64
|
+
|
|
65
|
+
// src/utils.ts
|
|
66
|
+
function noRef(v) {
|
|
67
|
+
return v;
|
|
68
|
+
}
|
|
69
|
+
function getPreferredMedia(body) {
|
|
70
|
+
if (Object.keys(body).length === 0)
|
|
71
|
+
return void 0;
|
|
72
|
+
if ("application/json" in body)
|
|
73
|
+
return body["application/json"];
|
|
74
|
+
return Object.values(body)[0];
|
|
75
|
+
}
|
|
76
|
+
function getValue(value) {
|
|
77
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/samples/index.ts
|
|
81
|
+
function createEndpoint(path, method, baseUrl = "https://example.com") {
|
|
82
|
+
const params = [];
|
|
83
|
+
const responses = {};
|
|
84
|
+
for (const param of method.parameters) {
|
|
85
|
+
const schema = noRef(
|
|
86
|
+
param.schema ?? getPreferredMedia(param.content ?? {})?.schema
|
|
87
|
+
);
|
|
88
|
+
if (!schema)
|
|
89
|
+
continue;
|
|
90
|
+
params.push({
|
|
91
|
+
name: param.name,
|
|
92
|
+
in: param.in,
|
|
93
|
+
schema
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const body = noRef(method.requestBody)?.content ?? {};
|
|
97
|
+
const bodySchema = noRef(getPreferredMedia(body)?.schema);
|
|
98
|
+
for (const [code, value] of Object.entries(method.responses)) {
|
|
99
|
+
const mediaTypes = noRef(value).content ?? {};
|
|
100
|
+
const responseSchema = noRef(getPreferredMedia(mediaTypes)?.schema);
|
|
101
|
+
if (!responseSchema)
|
|
102
|
+
continue;
|
|
103
|
+
responses[code] = {
|
|
104
|
+
schema: responseSchema
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
let pathWithParameters = path;
|
|
108
|
+
const queryParams = new URLSearchParams();
|
|
109
|
+
for (const param of params) {
|
|
110
|
+
const value = generateSample(method.method, param.schema);
|
|
111
|
+
if (param.in === "query")
|
|
112
|
+
queryParams.append(param.name, getValue(value));
|
|
113
|
+
if (param.in === "path")
|
|
114
|
+
pathWithParameters = pathWithParameters.replace(
|
|
115
|
+
`{${param.name}}`,
|
|
116
|
+
getValue(value)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
url: new URL(pathWithParameters, baseUrl).toString(),
|
|
121
|
+
body: bodySchema ? generateSample(method.method, bodySchema) : void 0,
|
|
122
|
+
responses,
|
|
123
|
+
method: method.method,
|
|
124
|
+
parameters: params
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function generateSample(method, schema) {
|
|
128
|
+
return sample(schema, {
|
|
129
|
+
skipReadOnly: method !== "GET",
|
|
130
|
+
skipWriteOnly: method === "GET"
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/samples/response.ts
|
|
135
|
+
function getExampleResponse(endpoint, code) {
|
|
136
|
+
if (code in endpoint.responses) {
|
|
137
|
+
const value = generateSample(
|
|
138
|
+
endpoint.method,
|
|
139
|
+
endpoint.responses[code].schema
|
|
140
|
+
);
|
|
141
|
+
return JSON.stringify(value, null, 2);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/samples/typescript.ts
|
|
146
|
+
import { compile } from "json-schema-to-typescript";
|
|
147
|
+
async function getTypescript(endpoint, code) {
|
|
148
|
+
if (code in endpoint.responses) {
|
|
149
|
+
return compile(endpoint.responses[code].schema, "Response", {
|
|
150
|
+
bannerComment: "",
|
|
151
|
+
additionalProperties: false,
|
|
152
|
+
format: true,
|
|
153
|
+
enableConstEnums: false
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/samples/curl.ts
|
|
159
|
+
function getSampleRequest(endpoint) {
|
|
160
|
+
const s = [];
|
|
161
|
+
s.push(`curl -X ${endpoint.method} "${endpoint.url}"`);
|
|
162
|
+
for (const param of endpoint.parameters) {
|
|
163
|
+
if (param.in === "header") {
|
|
164
|
+
const value = generateSample(endpoint.method, param.schema);
|
|
165
|
+
const header = `${param.name}: ${getValue2(value)}`;
|
|
166
|
+
s.push(`-H "${header}"`);
|
|
167
|
+
}
|
|
168
|
+
if (param.in === "formData") {
|
|
169
|
+
console.log("Request example for form data is not supported");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (endpoint.body)
|
|
173
|
+
s.push(`-d '${getValue2(endpoint.body)}'`);
|
|
174
|
+
return s.join(" \\\n ");
|
|
175
|
+
}
|
|
176
|
+
function getValue2(value) {
|
|
177
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/render/schema.ts
|
|
181
|
+
var keys = {
|
|
182
|
+
example: "Example",
|
|
183
|
+
default: "Default",
|
|
184
|
+
minimum: "Minimum",
|
|
185
|
+
maximum: "Maximum",
|
|
186
|
+
minLength: "Minimum length",
|
|
187
|
+
maxLength: "Maximum length",
|
|
188
|
+
pattern: "Pattern",
|
|
189
|
+
format: "Format"
|
|
190
|
+
};
|
|
191
|
+
function isObject(schema) {
|
|
192
|
+
return schema.type === "object" || schema.properties !== void 0;
|
|
193
|
+
}
|
|
194
|
+
function schemaElement(name, schema, { parseObject, ...ctx }) {
|
|
195
|
+
if (schema.readOnly && !ctx.readOnly)
|
|
196
|
+
return "";
|
|
197
|
+
if (schema.writeOnly && !ctx.writeOnly)
|
|
198
|
+
return "";
|
|
199
|
+
const child = [];
|
|
200
|
+
function field(key, value) {
|
|
201
|
+
child.push(span(`${key}: \`${value}\``));
|
|
202
|
+
}
|
|
203
|
+
if (isObject(schema) && parseObject) {
|
|
204
|
+
const { additionalProperties, properties } = schema;
|
|
205
|
+
if (additionalProperties) {
|
|
206
|
+
if (additionalProperties === true) {
|
|
207
|
+
child.push(
|
|
208
|
+
property({
|
|
209
|
+
name: "[key: string]",
|
|
210
|
+
type: "any"
|
|
211
|
+
})
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
child.push(
|
|
215
|
+
schemaElement("[key: string]", noRef(additionalProperties), {
|
|
216
|
+
...ctx,
|
|
217
|
+
required: false,
|
|
218
|
+
parseObject: false
|
|
219
|
+
})
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
Object.entries(properties ?? {}).forEach(([key, value]) => {
|
|
224
|
+
child.push(
|
|
225
|
+
schemaElement(key, noRef(value), {
|
|
226
|
+
...ctx,
|
|
227
|
+
required: schema.required?.includes(key) ?? false,
|
|
228
|
+
parseObject: false
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
return child.join("\n\n");
|
|
233
|
+
}
|
|
234
|
+
child.push(p(schema.description));
|
|
235
|
+
for (const [key, value] of Object.entries(keys)) {
|
|
236
|
+
if (key in schema) {
|
|
237
|
+
field(value, JSON.stringify(schema[key]));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (schema.enum) {
|
|
241
|
+
field(
|
|
242
|
+
"Value in",
|
|
243
|
+
schema.enum.map((value) => JSON.stringify(value)).join(" | ")
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
const resolved = resolveObjectType(schema);
|
|
247
|
+
if (resolved && !parseObject) {
|
|
248
|
+
child.push(
|
|
249
|
+
accordions(
|
|
250
|
+
accordion(
|
|
251
|
+
{ title: "Object Type" },
|
|
252
|
+
schemaElement(name, resolved, {
|
|
253
|
+
...ctx,
|
|
254
|
+
parseObject: true,
|
|
255
|
+
required: false
|
|
256
|
+
})
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return property(
|
|
262
|
+
{
|
|
263
|
+
name,
|
|
264
|
+
type: getSchemaType(schema),
|
|
265
|
+
required: ctx.required,
|
|
266
|
+
deprecated: schema.deprecated
|
|
267
|
+
},
|
|
268
|
+
...child
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
function resolveObjectType(schema) {
|
|
272
|
+
if (isObject(schema))
|
|
273
|
+
return schema;
|
|
274
|
+
if (schema.type === "array") {
|
|
275
|
+
return resolveObjectType(noRef(schema.items));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function getSchemaType(schema) {
|
|
279
|
+
if (schema.nullable) {
|
|
280
|
+
return `${getSchemaType({ ...schema, nullable: false })} | null`;
|
|
281
|
+
}
|
|
282
|
+
if (schema.type === "array")
|
|
283
|
+
return `array of ${getSchemaType(noRef(schema.items))}`;
|
|
284
|
+
if (schema.oneOf)
|
|
285
|
+
return schema.oneOf.map((one) => getSchemaType(noRef(one))).join(" | ");
|
|
286
|
+
if (schema.allOf)
|
|
287
|
+
return schema.allOf.map((one) => getSchemaType(noRef(one))).join(" & ");
|
|
288
|
+
if (schema.anyOf)
|
|
289
|
+
return `Any properties in ${schema.anyOf.map((one) => getSchemaType(noRef(one))).join(", ")}`;
|
|
290
|
+
if (schema.type)
|
|
291
|
+
return schema.type;
|
|
292
|
+
if (isObject(schema))
|
|
293
|
+
return "object";
|
|
294
|
+
throw new Error(`Cannot detect object type: ${JSON.stringify(schema)}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/render/operation.ts
|
|
298
|
+
async function renderOperation(path, method, baseUrl) {
|
|
299
|
+
const info = [];
|
|
300
|
+
const example = [];
|
|
301
|
+
info.push(`## ${method.summary ?? method.operationId}`);
|
|
302
|
+
if (method.description)
|
|
303
|
+
info.push(p(method.description));
|
|
304
|
+
const body = noRef(method.requestBody);
|
|
305
|
+
if (body) {
|
|
306
|
+
const bodySchema = getPreferredMedia(body.content)?.schema;
|
|
307
|
+
if (!bodySchema)
|
|
308
|
+
throw new Error();
|
|
309
|
+
info.push(
|
|
310
|
+
`### Request Body${!body.required ? " (Optional)" : ""}`,
|
|
311
|
+
p(body.description),
|
|
312
|
+
schemaElement("body", noRef(bodySchema), {
|
|
313
|
+
parseObject: true,
|
|
314
|
+
readOnly: method.method === "GET",
|
|
315
|
+
writeOnly: method.method !== "GET",
|
|
316
|
+
required: body.required ?? false
|
|
317
|
+
})
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const parameterGroups = /* @__PURE__ */ new Map();
|
|
321
|
+
const endpoint = createEndpoint(path, method, baseUrl);
|
|
322
|
+
for (const param of method.parameters) {
|
|
323
|
+
const schema = noRef(
|
|
324
|
+
param.schema ?? getPreferredMedia(param.content ?? {})?.schema
|
|
325
|
+
);
|
|
326
|
+
if (!schema)
|
|
327
|
+
continue;
|
|
328
|
+
const content = schemaElement(
|
|
329
|
+
param.name,
|
|
330
|
+
{
|
|
331
|
+
...schema,
|
|
332
|
+
description: param.description ?? schema.description,
|
|
333
|
+
deprecated: param.deprecated || schema.deprecated
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
parseObject: false,
|
|
337
|
+
readOnly: method.method === "GET",
|
|
338
|
+
writeOnly: method.method !== "GET",
|
|
339
|
+
required: param.required ?? false
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
const groupName = {
|
|
343
|
+
path: "Path Parameters",
|
|
344
|
+
query: "Query Parameters",
|
|
345
|
+
header: "Header Parameters",
|
|
346
|
+
cookie: "Cookie Parameters"
|
|
347
|
+
}[param.in] ?? "Other Parameters";
|
|
348
|
+
const group = parameterGroups.get(groupName) ?? [];
|
|
349
|
+
group.push(content);
|
|
350
|
+
parameterGroups.set(groupName, group);
|
|
351
|
+
}
|
|
352
|
+
for (const [group, parameters] of Array.from(parameterGroups.entries())) {
|
|
353
|
+
info.push(`### ${group}`, ...parameters);
|
|
354
|
+
}
|
|
355
|
+
info.push(getResponseTable(method));
|
|
356
|
+
example.push(
|
|
357
|
+
codeblock({ language: "bash", title: "curl" }, getSampleRequest(endpoint))
|
|
358
|
+
);
|
|
359
|
+
example.push(await getResponseTabs(endpoint, method));
|
|
360
|
+
return api(
|
|
361
|
+
apiInfo({ method: method.method, route: path }, ...info),
|
|
362
|
+
apiExample(...example)
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
function getResponseTable(operation) {
|
|
366
|
+
const table = [];
|
|
367
|
+
table.push(`| Status code | Description |`);
|
|
368
|
+
table.push(`| ----------- | ----------- |`);
|
|
369
|
+
Object.entries(operation.responses).forEach(([code, value]) => {
|
|
370
|
+
table.push(`| \`${code}\` | ${noRef(value).description} |`);
|
|
371
|
+
});
|
|
372
|
+
return table.join("\n");
|
|
373
|
+
}
|
|
374
|
+
async function getResponseTabs(endpoint, operation) {
|
|
375
|
+
const items = [];
|
|
376
|
+
const child = [];
|
|
377
|
+
for (const [code, _] of Object.entries(operation.responses)) {
|
|
378
|
+
const example = getExampleResponse(endpoint, code);
|
|
379
|
+
const ts = await getTypescript(endpoint, code);
|
|
380
|
+
const description = code in endpoint.responses ? endpoint.responses[code].schema.description : void 0;
|
|
381
|
+
if (example && ts) {
|
|
382
|
+
items.push(code);
|
|
383
|
+
child.push(
|
|
384
|
+
tab(
|
|
385
|
+
{ value: code },
|
|
386
|
+
p(description),
|
|
387
|
+
codeblock({ language: "json", title: "Example Response" }, example),
|
|
388
|
+
accordions(
|
|
389
|
+
accordion(
|
|
390
|
+
{ title: "Typescript Definition" },
|
|
391
|
+
codeblock({ language: "ts" }, ts)
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (items.length === 0)
|
|
399
|
+
return "";
|
|
400
|
+
return tabs(
|
|
401
|
+
{
|
|
402
|
+
items
|
|
403
|
+
},
|
|
404
|
+
...child
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/generate.ts
|
|
409
|
+
async function dereference(pathOrDocument) {
|
|
410
|
+
return await Parser.dereference(pathOrDocument);
|
|
411
|
+
}
|
|
412
|
+
async function generate(pathOrDocument, options = {}) {
|
|
413
|
+
const document = await dereference(pathOrDocument);
|
|
414
|
+
const tag = options.tag ? document.tags?.find((item) => item.name === options.tag) : void 0;
|
|
415
|
+
const routes = Object.entries(document.paths).map(
|
|
416
|
+
([key, value]) => {
|
|
417
|
+
if (!value)
|
|
418
|
+
throw new Error("Invalid schema");
|
|
419
|
+
const methodKeys = ["get", "post", "patch", "delete", "head"];
|
|
420
|
+
const methods = [];
|
|
421
|
+
for (const methodKey of methodKeys) {
|
|
422
|
+
const operation = value[methodKey];
|
|
423
|
+
if (!operation)
|
|
424
|
+
continue;
|
|
425
|
+
if (tag && !operation.tags?.includes(tag.name))
|
|
426
|
+
continue;
|
|
427
|
+
methods.push(buildOperation(methodKey, operation));
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
...value,
|
|
431
|
+
path: key,
|
|
432
|
+
methods
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
const serverUrl = document.servers?.[0].url;
|
|
437
|
+
const s = [];
|
|
438
|
+
for (const route of routes) {
|
|
439
|
+
for (const method of route.methods) {
|
|
440
|
+
s.push(await renderOperation(route.path, method, serverUrl));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return render(
|
|
444
|
+
tag?.name ?? document.info.title,
|
|
445
|
+
tag?.description ?? document.info.description,
|
|
446
|
+
root(...s),
|
|
447
|
+
options
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
async function generateTags(pathOrDocument, options = {}) {
|
|
451
|
+
const document = await dereference(pathOrDocument);
|
|
452
|
+
const results = document.tags?.map(async (tag) => {
|
|
453
|
+
return {
|
|
454
|
+
tag: tag.name,
|
|
455
|
+
content: await generate(document, {
|
|
456
|
+
tag: tag.name,
|
|
457
|
+
...options
|
|
458
|
+
})
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
return Promise.all(results ?? []);
|
|
462
|
+
}
|
|
463
|
+
function render(title, description, content, {
|
|
464
|
+
render: fn,
|
|
465
|
+
componentsImportPath = "fumadocs-ui/components/api"
|
|
466
|
+
}) {
|
|
467
|
+
const result = fn?.(title, description, content) ?? {};
|
|
468
|
+
const rendered = {
|
|
469
|
+
frontmatter: result.frontmatter ?? [
|
|
470
|
+
"---",
|
|
471
|
+
title && `title: ${title}`,
|
|
472
|
+
description && `description: ${description}`,
|
|
473
|
+
"---"
|
|
474
|
+
].filter(Boolean).join("\n"),
|
|
475
|
+
imports: result.imports ?? [
|
|
476
|
+
`import { Root, API, APIInfo, APIExample, Property } from '${componentsImportPath}'`,
|
|
477
|
+
`import { Tabs, Tab } from 'fumadocs-ui/components/tabs'`,
|
|
478
|
+
`import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';`
|
|
479
|
+
],
|
|
480
|
+
content: result.content ?? content
|
|
481
|
+
};
|
|
482
|
+
return [rendered.frontmatter, rendered.imports.join("\n"), rendered.content].filter(Boolean).join("\n\n");
|
|
483
|
+
}
|
|
484
|
+
function buildOperation(method, operation) {
|
|
485
|
+
return {
|
|
486
|
+
...operation,
|
|
487
|
+
parameters: operation.parameters ?? [],
|
|
488
|
+
method: method.toUpperCase()
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/generate-file.ts
|
|
493
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
494
|
+
import { dirname, join, parse } from "node:path";
|
|
495
|
+
import fg from "fast-glob";
|
|
496
|
+
async function generateFiles({
|
|
497
|
+
input,
|
|
498
|
+
output,
|
|
499
|
+
name: nameFn,
|
|
500
|
+
per = "file",
|
|
501
|
+
render: render2,
|
|
502
|
+
cwd = process.cwd()
|
|
503
|
+
}) {
|
|
504
|
+
const outputDir = join(cwd, output);
|
|
505
|
+
const options = {
|
|
506
|
+
render: render2
|
|
507
|
+
};
|
|
508
|
+
const resolvedInputs = await fg.glob(input, { absolute: true, cwd });
|
|
509
|
+
await Promise.all(
|
|
510
|
+
resolvedInputs.map(async (path) => {
|
|
511
|
+
let filename = parse(path).name;
|
|
512
|
+
filename = nameFn?.("file", filename) ?? filename;
|
|
513
|
+
if (per === "file") {
|
|
514
|
+
const outPath = join(outputDir, `${filename}.mdx`);
|
|
515
|
+
const result = await generate(path, options);
|
|
516
|
+
write(outPath, result);
|
|
517
|
+
console.log(`Generated: ${outPath}`);
|
|
518
|
+
} else {
|
|
519
|
+
const results = await generateTags(path, options);
|
|
520
|
+
results.forEach((result) => {
|
|
521
|
+
let tagName = result.tag;
|
|
522
|
+
tagName = nameFn?.("tag", tagName) ?? tagName.toLowerCase().replace(/\s+/g, "-");
|
|
523
|
+
const outPath = join(outputDir, filename, `${tagName}.mdx`);
|
|
524
|
+
write(outPath, result.content);
|
|
525
|
+
console.log(`Generated: ${outPath}`);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
})
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
function write(path, content) {
|
|
532
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
533
|
+
writeFileSync(path, content);
|
|
534
|
+
}
|
|
7
535
|
export {
|
|
8
536
|
dereference,
|
|
9
537
|
generate,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fumadocs-openapi",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Generate MDX docs for your OpenAPI spec",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"NextJs",
|
|
@@ -13,14 +13,12 @@
|
|
|
13
13
|
"type": "module",
|
|
14
14
|
"main": "./dist/index.js",
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
|
-
"bin": {
|
|
17
|
-
"fumadocs-openapi": "./dist/bin.js"
|
|
18
|
-
},
|
|
19
16
|
"files": [
|
|
20
17
|
"dist"
|
|
21
18
|
],
|
|
22
19
|
"dependencies": {
|
|
23
20
|
"@apidevtools/swagger-parser": "^10.1.0",
|
|
21
|
+
"fast-glob": "^3.3.1",
|
|
24
22
|
"json-schema-to-typescript": "^13.1.1",
|
|
25
23
|
"openapi-sampler": "^1.4.0"
|
|
26
24
|
},
|
package/dist/bin.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
package/dist/bin.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
generateFiles
|
|
4
|
-
} from "./chunk-6NCGY6WV.js";
|
|
5
|
-
|
|
6
|
-
// src/bin.ts
|
|
7
|
-
import { resolve } from "node:path";
|
|
8
|
-
import { pathToFileURL } from "node:url";
|
|
9
|
-
async function main() {
|
|
10
|
-
const configName = process.argv[2];
|
|
11
|
-
const config = await readConfig(configName);
|
|
12
|
-
await generateFiles(config);
|
|
13
|
-
}
|
|
14
|
-
async function readConfig(name = "openapi.config.js") {
|
|
15
|
-
const path = resolve(process.cwd(), name);
|
|
16
|
-
const result = await import(pathToFileURL(path).toString());
|
|
17
|
-
if (typeof result.default !== "object")
|
|
18
|
-
throw new Error("Invalid configuration");
|
|
19
|
-
return result.default;
|
|
20
|
-
}
|
|
21
|
-
void main();
|
package/dist/chunk-6NCGY6WV.js
DELETED
|
@@ -1,539 +0,0 @@
|
|
|
1
|
-
// src/generate.ts
|
|
2
|
-
import Parser from "@apidevtools/swagger-parser";
|
|
3
|
-
|
|
4
|
-
// src/render/element.ts
|
|
5
|
-
function createElement(name, props, ...child) {
|
|
6
|
-
const s = [];
|
|
7
|
-
const params = Object.entries(props).map(([key, value]) => `${key}={${JSON.stringify(value)}}`).join(" ");
|
|
8
|
-
s.push(params.length > 0 ? `<${name} ${params}>` : `<${name}>`);
|
|
9
|
-
s.push(...child);
|
|
10
|
-
s.push(`</${name}>`);
|
|
11
|
-
return s.join("\n\n");
|
|
12
|
-
}
|
|
13
|
-
function p(child) {
|
|
14
|
-
if (!child)
|
|
15
|
-
return "";
|
|
16
|
-
return child.replace("<", "\\<").replace(">", "\\>");
|
|
17
|
-
}
|
|
18
|
-
function span(child) {
|
|
19
|
-
return `<span>${p(child)}</span>`;
|
|
20
|
-
}
|
|
21
|
-
function codeblock({ language, title }, child) {
|
|
22
|
-
return [
|
|
23
|
-
title ? `\`\`\`${language} title=${JSON.stringify(title)}` : `\`\`\`${language}`,
|
|
24
|
-
child,
|
|
25
|
-
"```"
|
|
26
|
-
].join("\n");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// src/render/custom.ts
|
|
30
|
-
function api(...child) {
|
|
31
|
-
return createElement("API", {}, ...child);
|
|
32
|
-
}
|
|
33
|
-
function apiExample(...child) {
|
|
34
|
-
return createElement("APIExample", {}, ...child);
|
|
35
|
-
}
|
|
36
|
-
function root(...child) {
|
|
37
|
-
return createElement("Root", {}, ...child);
|
|
38
|
-
}
|
|
39
|
-
function apiInfo(props, ...child) {
|
|
40
|
-
return createElement("APIInfo", props, ...child);
|
|
41
|
-
}
|
|
42
|
-
function accordions(...child) {
|
|
43
|
-
return createElement("Accordions", {}, ...child);
|
|
44
|
-
}
|
|
45
|
-
function accordion(props, ...child) {
|
|
46
|
-
return createElement("Accordion", props, ...child);
|
|
47
|
-
}
|
|
48
|
-
function tabs(props, ...child) {
|
|
49
|
-
return createElement("Tabs", props, ...child);
|
|
50
|
-
}
|
|
51
|
-
function tab(props, ...child) {
|
|
52
|
-
return createElement("Tab", props, ...child);
|
|
53
|
-
}
|
|
54
|
-
function property({ required = false, deprecated = false, ...props }, ...child) {
|
|
55
|
-
return createElement(
|
|
56
|
-
"Property",
|
|
57
|
-
{ required, deprecated, ...props },
|
|
58
|
-
...child
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// src/samples/index.ts
|
|
63
|
-
import { sample } from "openapi-sampler";
|
|
64
|
-
|
|
65
|
-
// src/utils.ts
|
|
66
|
-
function noRef(v) {
|
|
67
|
-
return v;
|
|
68
|
-
}
|
|
69
|
-
function getPreferredMedia(body) {
|
|
70
|
-
if (Object.keys(body).length === 0)
|
|
71
|
-
return void 0;
|
|
72
|
-
if ("application/json" in body)
|
|
73
|
-
return body["application/json"];
|
|
74
|
-
return Object.values(body)[0];
|
|
75
|
-
}
|
|
76
|
-
function getValue(value) {
|
|
77
|
-
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// src/samples/index.ts
|
|
81
|
-
function createEndpoint(path, method, baseUrl = "https://example.com") {
|
|
82
|
-
const params = [];
|
|
83
|
-
const responses = {};
|
|
84
|
-
for (const param of method.parameters) {
|
|
85
|
-
const schema = noRef(
|
|
86
|
-
param.schema ?? getPreferredMedia(param.content ?? {})?.schema
|
|
87
|
-
);
|
|
88
|
-
if (!schema)
|
|
89
|
-
continue;
|
|
90
|
-
params.push({
|
|
91
|
-
name: param.name,
|
|
92
|
-
in: param.in,
|
|
93
|
-
schema
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
const body = noRef(method.requestBody)?.content ?? {};
|
|
97
|
-
const bodySchema = noRef(getPreferredMedia(body)?.schema);
|
|
98
|
-
for (const [code, value] of Object.entries(method.responses)) {
|
|
99
|
-
const mediaTypes = noRef(value).content ?? {};
|
|
100
|
-
const responseSchema = noRef(getPreferredMedia(mediaTypes)?.schema);
|
|
101
|
-
if (!responseSchema)
|
|
102
|
-
continue;
|
|
103
|
-
responses[code] = {
|
|
104
|
-
schema: responseSchema
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
let pathWithParameters = path;
|
|
108
|
-
const queryParams = new URLSearchParams();
|
|
109
|
-
for (const param of params) {
|
|
110
|
-
const value = generateSample(method.method, param.schema);
|
|
111
|
-
if (param.in === "query")
|
|
112
|
-
queryParams.append(param.name, getValue(value));
|
|
113
|
-
if (param.in === "path")
|
|
114
|
-
pathWithParameters = pathWithParameters.replace(
|
|
115
|
-
`{${param.name}}`,
|
|
116
|
-
getValue(value)
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
return {
|
|
120
|
-
url: new URL(pathWithParameters, baseUrl).toString(),
|
|
121
|
-
body: bodySchema ? generateSample(method.method, bodySchema) : void 0,
|
|
122
|
-
responses,
|
|
123
|
-
method: method.method,
|
|
124
|
-
parameters: params
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
function generateSample(method, schema) {
|
|
128
|
-
return sample(schema, {
|
|
129
|
-
skipReadOnly: method !== "GET",
|
|
130
|
-
skipWriteOnly: method === "GET"
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// src/samples/response.ts
|
|
135
|
-
function getExampleResponse(endpoint, code) {
|
|
136
|
-
if (code in endpoint.responses) {
|
|
137
|
-
const value = generateSample(
|
|
138
|
-
endpoint.method,
|
|
139
|
-
endpoint.responses[code].schema
|
|
140
|
-
);
|
|
141
|
-
return JSON.stringify(value, null, 2);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// src/samples/typescript.ts
|
|
146
|
-
import { compile } from "json-schema-to-typescript";
|
|
147
|
-
async function getTypescript(endpoint, code) {
|
|
148
|
-
if (code in endpoint.responses) {
|
|
149
|
-
return compile(endpoint.responses[code].schema, "Response", {
|
|
150
|
-
bannerComment: "",
|
|
151
|
-
additionalProperties: false,
|
|
152
|
-
format: true,
|
|
153
|
-
enableConstEnums: false
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// src/samples/curl.ts
|
|
159
|
-
function getSampleRequest(endpoint) {
|
|
160
|
-
const s = [];
|
|
161
|
-
s.push(`curl -X ${endpoint.method} "${endpoint.url}"`);
|
|
162
|
-
for (const param of endpoint.parameters) {
|
|
163
|
-
if (param.in === "header") {
|
|
164
|
-
const value = generateSample(endpoint.method, param.schema);
|
|
165
|
-
const header = `${param.name}: ${getValue2(value)}`;
|
|
166
|
-
s.push(`-H "${header}"`);
|
|
167
|
-
}
|
|
168
|
-
if (param.in === "formData") {
|
|
169
|
-
console.log("Request example for form data is not supported");
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (endpoint.body)
|
|
173
|
-
s.push(`-d '${getValue2(endpoint.body)}'`);
|
|
174
|
-
return s.join(" \\\n ");
|
|
175
|
-
}
|
|
176
|
-
function getValue2(value) {
|
|
177
|
-
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// src/render/schema.ts
|
|
181
|
-
var keys = {
|
|
182
|
-
example: "Example",
|
|
183
|
-
default: "Default",
|
|
184
|
-
minimum: "Minimum",
|
|
185
|
-
maximum: "Maximum",
|
|
186
|
-
minLength: "Minimum length",
|
|
187
|
-
maxLength: "Maximum length",
|
|
188
|
-
pattern: "Pattern",
|
|
189
|
-
format: "Format"
|
|
190
|
-
};
|
|
191
|
-
function isObject(schema) {
|
|
192
|
-
return schema.type === "object" || schema.properties !== void 0;
|
|
193
|
-
}
|
|
194
|
-
function schemaElement(name, schema, { parseObject, ...ctx }) {
|
|
195
|
-
if (schema.readOnly && !ctx.readOnly)
|
|
196
|
-
return "";
|
|
197
|
-
if (schema.writeOnly && !ctx.writeOnly)
|
|
198
|
-
return "";
|
|
199
|
-
const child = [];
|
|
200
|
-
function field(key, value) {
|
|
201
|
-
child.push(span(`${key}: \`${value}\``));
|
|
202
|
-
}
|
|
203
|
-
if (isObject(schema) && parseObject) {
|
|
204
|
-
const { additionalProperties, properties } = schema;
|
|
205
|
-
if (additionalProperties) {
|
|
206
|
-
if (additionalProperties === true) {
|
|
207
|
-
child.push(
|
|
208
|
-
property({
|
|
209
|
-
name: "[key: string]",
|
|
210
|
-
type: "any"
|
|
211
|
-
})
|
|
212
|
-
);
|
|
213
|
-
} else {
|
|
214
|
-
child.push(
|
|
215
|
-
schemaElement("[key: string]", noRef(additionalProperties), {
|
|
216
|
-
...ctx,
|
|
217
|
-
required: false,
|
|
218
|
-
parseObject: false
|
|
219
|
-
})
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
Object.entries(properties ?? {}).forEach(([key, value]) => {
|
|
224
|
-
child.push(
|
|
225
|
-
schemaElement(key, noRef(value), {
|
|
226
|
-
...ctx,
|
|
227
|
-
required: schema.required?.includes(key) ?? false,
|
|
228
|
-
parseObject: false
|
|
229
|
-
})
|
|
230
|
-
);
|
|
231
|
-
});
|
|
232
|
-
return child.join("\n\n");
|
|
233
|
-
}
|
|
234
|
-
child.push(p(schema.description));
|
|
235
|
-
for (const [key, value] of Object.entries(keys)) {
|
|
236
|
-
if (key in schema) {
|
|
237
|
-
field(value, JSON.stringify(schema[key]));
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
if (schema.enum) {
|
|
241
|
-
field(
|
|
242
|
-
"Value in",
|
|
243
|
-
schema.enum.map((value) => JSON.stringify(value)).join(" | ")
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
const resolved = resolveObjectType(schema);
|
|
247
|
-
if (resolved && !parseObject) {
|
|
248
|
-
child.push(
|
|
249
|
-
accordions(
|
|
250
|
-
accordion(
|
|
251
|
-
{ title: "Object Type" },
|
|
252
|
-
schemaElement(name, resolved, {
|
|
253
|
-
...ctx,
|
|
254
|
-
parseObject: true,
|
|
255
|
-
required: false
|
|
256
|
-
})
|
|
257
|
-
)
|
|
258
|
-
)
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
return property(
|
|
262
|
-
{
|
|
263
|
-
name,
|
|
264
|
-
type: getSchemaType(schema),
|
|
265
|
-
required: ctx.required,
|
|
266
|
-
deprecated: schema.deprecated
|
|
267
|
-
},
|
|
268
|
-
...child
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
function resolveObjectType(schema) {
|
|
272
|
-
if (isObject(schema))
|
|
273
|
-
return schema;
|
|
274
|
-
if (schema.type === "array") {
|
|
275
|
-
return resolveObjectType(noRef(schema.items));
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
function getSchemaType(schema) {
|
|
279
|
-
if (schema.nullable) {
|
|
280
|
-
return `${getSchemaType({ ...schema, nullable: false })} | null`;
|
|
281
|
-
}
|
|
282
|
-
if (schema.type === "array")
|
|
283
|
-
return `array of ${getSchemaType(noRef(schema.items))}`;
|
|
284
|
-
if (schema.oneOf)
|
|
285
|
-
return schema.oneOf.map((one) => getSchemaType(noRef(one))).join(" | ");
|
|
286
|
-
if (schema.allOf)
|
|
287
|
-
return schema.allOf.map((one) => getSchemaType(noRef(one))).join(" & ");
|
|
288
|
-
if (schema.anyOf)
|
|
289
|
-
return `Any properties in ${schema.anyOf.map((one) => getSchemaType(noRef(one))).join(", ")}`;
|
|
290
|
-
if (schema.type)
|
|
291
|
-
return schema.type;
|
|
292
|
-
if (isObject(schema))
|
|
293
|
-
return "object";
|
|
294
|
-
throw new Error(`Cannot detect object type: ${JSON.stringify(schema)}`);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// src/render/operation.ts
|
|
298
|
-
async function renderOperation(path, method, baseUrl) {
|
|
299
|
-
const info = [];
|
|
300
|
-
const example = [];
|
|
301
|
-
info.push(`## ${method.summary ?? method.operationId}`);
|
|
302
|
-
if (method.description)
|
|
303
|
-
info.push(p(method.description));
|
|
304
|
-
const body = noRef(method.requestBody);
|
|
305
|
-
if (body) {
|
|
306
|
-
const bodySchema = getPreferredMedia(body.content)?.schema;
|
|
307
|
-
if (!bodySchema)
|
|
308
|
-
throw new Error();
|
|
309
|
-
info.push(
|
|
310
|
-
`### Request Body${!body.required ? " (Optional)" : ""}`,
|
|
311
|
-
p(body.description),
|
|
312
|
-
schemaElement("body", noRef(bodySchema), {
|
|
313
|
-
parseObject: true,
|
|
314
|
-
readOnly: method.method === "GET",
|
|
315
|
-
writeOnly: method.method !== "GET",
|
|
316
|
-
required: body.required ?? false
|
|
317
|
-
})
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
const parameterGroups = /* @__PURE__ */ new Map();
|
|
321
|
-
const endpoint = createEndpoint(path, method, baseUrl);
|
|
322
|
-
for (const param of method.parameters) {
|
|
323
|
-
const schema = noRef(
|
|
324
|
-
param.schema ?? getPreferredMedia(param.content ?? {})?.schema
|
|
325
|
-
);
|
|
326
|
-
if (!schema)
|
|
327
|
-
continue;
|
|
328
|
-
const content = schemaElement(
|
|
329
|
-
param.name,
|
|
330
|
-
{
|
|
331
|
-
...schema,
|
|
332
|
-
description: param.description ?? schema.description,
|
|
333
|
-
deprecated: param.deprecated || schema.deprecated
|
|
334
|
-
},
|
|
335
|
-
{
|
|
336
|
-
parseObject: false,
|
|
337
|
-
readOnly: method.method === "GET",
|
|
338
|
-
writeOnly: method.method !== "GET",
|
|
339
|
-
required: param.required ?? false
|
|
340
|
-
}
|
|
341
|
-
);
|
|
342
|
-
const groupName = {
|
|
343
|
-
path: "Path Parameters",
|
|
344
|
-
query: "Query Parameters",
|
|
345
|
-
header: "Header Parameters",
|
|
346
|
-
cookie: "Cookie Parameters"
|
|
347
|
-
}[param.in] ?? "Other Parameters";
|
|
348
|
-
const group = parameterGroups.get(groupName) ?? [];
|
|
349
|
-
group.push(content);
|
|
350
|
-
parameterGroups.set(groupName, group);
|
|
351
|
-
}
|
|
352
|
-
for (const [group, parameters] of Array.from(parameterGroups.entries())) {
|
|
353
|
-
info.push(`### ${group}`, ...parameters);
|
|
354
|
-
}
|
|
355
|
-
info.push(getResponseTable(method));
|
|
356
|
-
example.push(
|
|
357
|
-
codeblock({ language: "bash", title: "curl" }, getSampleRequest(endpoint))
|
|
358
|
-
);
|
|
359
|
-
example.push(await getResponseTabs(endpoint, method));
|
|
360
|
-
return api(
|
|
361
|
-
apiInfo({ method: method.method, route: path }, ...info),
|
|
362
|
-
apiExample(...example)
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
function getResponseTable(operation) {
|
|
366
|
-
const table = [];
|
|
367
|
-
table.push(`| Status code | Description |`);
|
|
368
|
-
table.push(`| ----------- | ----------- |`);
|
|
369
|
-
Object.entries(operation.responses).forEach(([code, value]) => {
|
|
370
|
-
table.push(`| \`${code}\` | ${noRef(value).description} |`);
|
|
371
|
-
});
|
|
372
|
-
return table.join("\n");
|
|
373
|
-
}
|
|
374
|
-
async function getResponseTabs(endpoint, operation) {
|
|
375
|
-
const items = [];
|
|
376
|
-
const child = [];
|
|
377
|
-
for (const [code, _] of Object.entries(operation.responses)) {
|
|
378
|
-
const example = getExampleResponse(endpoint, code);
|
|
379
|
-
const ts = await getTypescript(endpoint, code);
|
|
380
|
-
const description = code in endpoint.responses ? endpoint.responses[code].schema.description : void 0;
|
|
381
|
-
if (example && ts) {
|
|
382
|
-
items.push(code);
|
|
383
|
-
child.push(
|
|
384
|
-
tab(
|
|
385
|
-
{ value: code },
|
|
386
|
-
p(description),
|
|
387
|
-
codeblock({ language: "json", title: "Example Response" }, example),
|
|
388
|
-
accordions(
|
|
389
|
-
accordion(
|
|
390
|
-
{ title: "Typescript Definition" },
|
|
391
|
-
codeblock({ language: "ts" }, ts)
|
|
392
|
-
)
|
|
393
|
-
)
|
|
394
|
-
)
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
if (items.length === 0)
|
|
399
|
-
return "";
|
|
400
|
-
return tabs(
|
|
401
|
-
{
|
|
402
|
-
items
|
|
403
|
-
},
|
|
404
|
-
...child
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// src/generate.ts
|
|
409
|
-
async function dereference(pathOrDocument) {
|
|
410
|
-
return await Parser.dereference(pathOrDocument);
|
|
411
|
-
}
|
|
412
|
-
async function generate(pathOrDocument, options = {}) {
|
|
413
|
-
const document = await dereference(pathOrDocument);
|
|
414
|
-
const tag = options.tag ? document.tags?.find((item) => item.name === options.tag) : void 0;
|
|
415
|
-
const routes = Object.entries(document.paths).map(
|
|
416
|
-
([key, value]) => {
|
|
417
|
-
if (!value)
|
|
418
|
-
throw new Error("Invalid schema");
|
|
419
|
-
const methodKeys = ["get", "post", "patch", "delete", "head"];
|
|
420
|
-
const methods = [];
|
|
421
|
-
for (const methodKey of methodKeys) {
|
|
422
|
-
const operation = value[methodKey];
|
|
423
|
-
if (!operation)
|
|
424
|
-
continue;
|
|
425
|
-
if (tag && !operation.tags?.includes(tag.name))
|
|
426
|
-
continue;
|
|
427
|
-
methods.push(buildOperation(methodKey, operation));
|
|
428
|
-
}
|
|
429
|
-
return {
|
|
430
|
-
...value,
|
|
431
|
-
path: key,
|
|
432
|
-
methods
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
);
|
|
436
|
-
const serverUrl = document.servers?.[0].url;
|
|
437
|
-
const s = [];
|
|
438
|
-
for (const route of routes) {
|
|
439
|
-
for (const method of route.methods) {
|
|
440
|
-
s.push(await renderOperation(route.path, method, serverUrl));
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
return render(
|
|
444
|
-
tag?.name ?? document.info.title,
|
|
445
|
-
tag?.description ?? document.info.description,
|
|
446
|
-
root(...s),
|
|
447
|
-
options
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
async function generateTags(pathOrDocument, options = {}) {
|
|
451
|
-
const document = await dereference(pathOrDocument);
|
|
452
|
-
const results = document.tags?.map(async (tag) => {
|
|
453
|
-
return {
|
|
454
|
-
tag: tag.name,
|
|
455
|
-
content: await generate(document, {
|
|
456
|
-
tag: tag.name,
|
|
457
|
-
...options
|
|
458
|
-
})
|
|
459
|
-
};
|
|
460
|
-
});
|
|
461
|
-
return Promise.all(results ?? []);
|
|
462
|
-
}
|
|
463
|
-
function render(title, description, content, {
|
|
464
|
-
render: fn,
|
|
465
|
-
componentsImportPath = "fumadocs-ui/components/api"
|
|
466
|
-
}) {
|
|
467
|
-
const result = fn?.(title, description, content) ?? {};
|
|
468
|
-
const rendered = {
|
|
469
|
-
frontmatter: result.frontmatter ?? [
|
|
470
|
-
"---",
|
|
471
|
-
title && `title: ${title}`,
|
|
472
|
-
description && `description: ${description}`,
|
|
473
|
-
"---"
|
|
474
|
-
].filter(Boolean).join("\n"),
|
|
475
|
-
imports: result.imports ?? [
|
|
476
|
-
`import { Root, API, APIInfo, APIExample, Property } from '${componentsImportPath}'`,
|
|
477
|
-
`import { Tabs, Tab } from 'fumadocs-ui/components/tabs'`,
|
|
478
|
-
`import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';`
|
|
479
|
-
],
|
|
480
|
-
content: result.content ?? content
|
|
481
|
-
};
|
|
482
|
-
return [rendered.frontmatter, rendered.imports.join("\n"), rendered.content].filter(Boolean).join("\n\n");
|
|
483
|
-
}
|
|
484
|
-
function buildOperation(method, operation) {
|
|
485
|
-
return {
|
|
486
|
-
...operation,
|
|
487
|
-
parameters: operation.parameters ?? [],
|
|
488
|
-
method: method.toUpperCase()
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// src/generate-file.ts
|
|
493
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
494
|
-
import { resolve, dirname, join, parse } from "node:path";
|
|
495
|
-
async function generateFiles({
|
|
496
|
-
input,
|
|
497
|
-
output,
|
|
498
|
-
name: nameFn,
|
|
499
|
-
per = "file",
|
|
500
|
-
render: render2
|
|
501
|
-
}) {
|
|
502
|
-
const outputDir = resolve(output);
|
|
503
|
-
const options = {
|
|
504
|
-
render: render2
|
|
505
|
-
};
|
|
506
|
-
await Promise.all(
|
|
507
|
-
input.map(async (file) => {
|
|
508
|
-
const path = resolve(file);
|
|
509
|
-
let filename = parse(path).name;
|
|
510
|
-
filename = nameFn?.("file", filename) ?? filename;
|
|
511
|
-
if (per === "file") {
|
|
512
|
-
const outPath = join(outputDir, `${filename}.mdx`);
|
|
513
|
-
const result = await generate(path, options);
|
|
514
|
-
write(outPath, result);
|
|
515
|
-
console.log(`Generated: ${outPath}`);
|
|
516
|
-
} else {
|
|
517
|
-
const results = await generateTags(path, options);
|
|
518
|
-
results.forEach((result) => {
|
|
519
|
-
let tagName = result.tag;
|
|
520
|
-
tagName = nameFn?.("tag", tagName) ?? tagName.toLowerCase().replace(/\s+/g, "-");
|
|
521
|
-
const outPath = join(outputDir, `${filename}/${tagName}.mdx`);
|
|
522
|
-
write(outPath, result.content);
|
|
523
|
-
console.log(`Generated: ${outPath}`);
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
})
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
function write(path, content) {
|
|
530
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
531
|
-
writeFileSync(path, content);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
export {
|
|
535
|
-
dereference,
|
|
536
|
-
generate,
|
|
537
|
-
generateTags,
|
|
538
|
-
generateFiles
|
|
539
|
-
};
|