fumadocs-openapi 1.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/LICENSE +21 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +21 -0
- package/dist/chunk-6NCGY6WV.js +539 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +12 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Fuma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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();
|
|
@@ -0,0 +1,539 @@
|
|
|
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
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
2
|
+
|
|
3
|
+
declare function dereference(pathOrDocument: string | OpenAPIV3.Document): Promise<OpenAPIV3.Document>;
|
|
4
|
+
interface GenerateOptions {
|
|
5
|
+
tag?: string;
|
|
6
|
+
/**
|
|
7
|
+
* The import path of your API components, it must exports all components in `fumadocs-ui/components/api`
|
|
8
|
+
*
|
|
9
|
+
* @defaultValue `fumadocs-ui/components/api`
|
|
10
|
+
*/
|
|
11
|
+
componentsImportPath?: string;
|
|
12
|
+
render?: (title: string | undefined, description: string | undefined, content: string) => Partial<RenderResult>;
|
|
13
|
+
}
|
|
14
|
+
interface RenderResult {
|
|
15
|
+
frontmatter: string;
|
|
16
|
+
imports: string[];
|
|
17
|
+
content: string;
|
|
18
|
+
}
|
|
19
|
+
declare function generate(pathOrDocument: string | OpenAPIV3.Document, options?: GenerateOptions): Promise<string>;
|
|
20
|
+
declare function generateTags(pathOrDocument: string | OpenAPIV3.Document, options?: Omit<GenerateOptions, 'tag'>): Promise<{
|
|
21
|
+
tag: string;
|
|
22
|
+
content: string;
|
|
23
|
+
}[]>;
|
|
24
|
+
|
|
25
|
+
interface Config {
|
|
26
|
+
/**
|
|
27
|
+
* Schema files
|
|
28
|
+
*/
|
|
29
|
+
input: string[];
|
|
30
|
+
/**
|
|
31
|
+
* Output directory
|
|
32
|
+
*/
|
|
33
|
+
output: string;
|
|
34
|
+
/**
|
|
35
|
+
* tag: Generate a page for each tag
|
|
36
|
+
*
|
|
37
|
+
* file: Generate a page for each schema
|
|
38
|
+
*
|
|
39
|
+
* @defaultValue tag
|
|
40
|
+
*/
|
|
41
|
+
per?: 'tag' | 'file';
|
|
42
|
+
/**
|
|
43
|
+
* Specify name for output file
|
|
44
|
+
*/
|
|
45
|
+
name?: (type: 'file' | 'tag', name: string) => string;
|
|
46
|
+
/**
|
|
47
|
+
* Modify output file
|
|
48
|
+
*/
|
|
49
|
+
render?: NonNullable<GenerateOptions['render']>;
|
|
50
|
+
}
|
|
51
|
+
declare function generateFiles({ input, output, name: nameFn, per, render, }: Config): Promise<void>;
|
|
52
|
+
|
|
53
|
+
interface RouteInformation {
|
|
54
|
+
path: string;
|
|
55
|
+
summary?: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
methods: MethodInformation[];
|
|
58
|
+
}
|
|
59
|
+
interface MethodInformation extends OpenAPIV3.OperationObject {
|
|
60
|
+
parameters: OpenAPIV3.ParameterObject[];
|
|
61
|
+
method: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { type Config, type GenerateOptions, type MethodInformation, type RouteInformation, dereference, generate, generateFiles, generateTags };
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fumadocs-openapi",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Generate MDX docs for your OpenAPI spec",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"NextJs",
|
|
7
|
+
"Docs"
|
|
8
|
+
],
|
|
9
|
+
"homepage": "https://fumadocs.vercel.app",
|
|
10
|
+
"repository": "github:fuma-nama/next-docs",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Fuma Nama",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"bin": {
|
|
17
|
+
"fumadocs-openapi": "./dist/bin.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@apidevtools/swagger-parser": "^10.1.0",
|
|
24
|
+
"json-schema-to-typescript": "^13.1.1",
|
|
25
|
+
"openapi-sampler": "^1.4.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "18.17.5",
|
|
29
|
+
"@types/openapi-sampler": "^1.0.3",
|
|
30
|
+
"openapi-types": "^12.1.3",
|
|
31
|
+
"eslint-config-custom": "0.0.0",
|
|
32
|
+
"tsconfig": "0.0.0"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"clean": "rimraf dist",
|
|
40
|
+
"dev": "tsup --watch",
|
|
41
|
+
"lint": "eslint .",
|
|
42
|
+
"test": "vitest",
|
|
43
|
+
"types:check": "tsc --noEmit"
|
|
44
|
+
}
|
|
45
|
+
}
|