fumadocs-openapi 3.0.0 → 3.1.1
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 +15 -2
- package/dist/index.js +212 -46
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,11 @@ interface PropertyProps {
|
|
|
19
19
|
interface ObjectCollapsibleProps {
|
|
20
20
|
name: string;
|
|
21
21
|
}
|
|
22
|
+
interface RequestProps {
|
|
23
|
+
language: string;
|
|
24
|
+
name: string;
|
|
25
|
+
code: string;
|
|
26
|
+
}
|
|
22
27
|
interface Renderer {
|
|
23
28
|
Root: (child: string[]) => string;
|
|
24
29
|
API: (child: string[]) => string;
|
|
@@ -26,6 +31,8 @@ interface Renderer {
|
|
|
26
31
|
APIExample: (child: string[]) => string;
|
|
27
32
|
Responses: (props: ResponsesProps, child: string[]) => string;
|
|
28
33
|
Response: (props: ResponseProps, child: string[]) => string;
|
|
34
|
+
Requests: (items: string[], child: string[]) => string;
|
|
35
|
+
Request: (props: RequestProps) => string;
|
|
29
36
|
ResponseTypes: (child: string[]) => string;
|
|
30
37
|
ExampleResponse: (json: string) => string;
|
|
31
38
|
TypeScriptResponse: (code: string) => string;
|
|
@@ -57,7 +64,12 @@ interface GenerateTagOutput {
|
|
|
57
64
|
tag: string;
|
|
58
65
|
content: string;
|
|
59
66
|
}
|
|
67
|
+
interface GenerateOperationOutput {
|
|
68
|
+
id: string;
|
|
69
|
+
content: string;
|
|
70
|
+
}
|
|
60
71
|
declare function generate(pathOrDocument: string | OpenAPIV3.Document, options?: GenerateOptions): Promise<string>;
|
|
72
|
+
declare function generateOperations(pathOrDocument: string | OpenAPIV3.Document, options?: GenerateOptions): Promise<GenerateOperationOutput[]>;
|
|
61
73
|
declare function generateTags(pathOrDocument: string | OpenAPIV3.Document, options?: GenerateOptions): Promise<GenerateTagOutput[]>;
|
|
62
74
|
|
|
63
75
|
interface Config extends GenerateOptions {
|
|
@@ -76,7 +88,7 @@ interface Config extends GenerateOptions {
|
|
|
76
88
|
*
|
|
77
89
|
* @defaultValue file
|
|
78
90
|
*/
|
|
79
|
-
per?: 'tag' | 'file';
|
|
91
|
+
per?: 'tag' | 'file' | 'operation';
|
|
80
92
|
/**
|
|
81
93
|
* Specify name for output file
|
|
82
94
|
*/
|
|
@@ -97,9 +109,10 @@ interface MethodInformation extends OpenAPIV3.OperationObject {
|
|
|
97
109
|
}
|
|
98
110
|
interface RenderContext {
|
|
99
111
|
renderer: Renderer;
|
|
112
|
+
document: OpenAPIV3.Document;
|
|
100
113
|
baseUrl: string;
|
|
101
114
|
}
|
|
102
115
|
|
|
103
116
|
declare function createElement(name: string, props: object, ...child: string[]): string;
|
|
104
117
|
|
|
105
|
-
export { type APIInfoProps, type Config, type GenerateOptions, type GenerateTagOutput, type MethodInformation, type ObjectCollapsibleProps, type PropertyProps, type RenderContext, type Renderer, type ResponseProps, type ResponsesProps, type RouteInformation, createElement, defaultRenderer, generate, generateFiles, generateTags };
|
|
118
|
+
export { type APIInfoProps, type Config, type GenerateOperationOutput, type GenerateOptions, type GenerateTagOutput, type MethodInformation, type ObjectCollapsibleProps, type PropertyProps, type RenderContext, type Renderer, type RequestProps, type ResponseProps, type ResponsesProps, type RouteInformation, createElement, defaultRenderer, generate, generateFiles, generateOperations, generateTags };
|
package/dist/index.js
CHANGED
|
@@ -72,6 +72,9 @@ function codeblock({ language, title }, child) {
|
|
|
72
72
|
"```"
|
|
73
73
|
].join("\n");
|
|
74
74
|
}
|
|
75
|
+
function heading(depth, child) {
|
|
76
|
+
return `${"#".repeat(depth)} ${child.trim()}`;
|
|
77
|
+
}
|
|
75
78
|
|
|
76
79
|
// src/render/renderer.ts
|
|
77
80
|
var defaultRenderer = {
|
|
@@ -89,7 +92,9 @@ var defaultRenderer = {
|
|
|
89
92
|
codeblock({ language: "ts" }, code)
|
|
90
93
|
),
|
|
91
94
|
Property: (props, child) => createElement("Property", props, ...child),
|
|
92
|
-
ObjectCollapsible: (props, child) => createElement("ObjectCollapsible", props, ...child)
|
|
95
|
+
ObjectCollapsible: (props, child) => createElement("ObjectCollapsible", props, ...child),
|
|
96
|
+
Requests: (items, child) => createElement("Requests", { items }, ...child),
|
|
97
|
+
Request: ({ language, code, name }) => createElement("Request", { value: name }, codeblock({ language }, code))
|
|
93
98
|
};
|
|
94
99
|
|
|
95
100
|
// src/render/page.ts
|
|
@@ -117,10 +122,7 @@ ${finalImports}
|
|
|
117
122
|
${Root(content)}`;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
// src/
|
|
121
|
-
import { sample } from "openapi-sampler";
|
|
122
|
-
|
|
123
|
-
// src/utils.ts
|
|
125
|
+
// src/utils/schema.ts
|
|
124
126
|
function noRef(v) {
|
|
125
127
|
return v;
|
|
126
128
|
}
|
|
@@ -129,11 +131,20 @@ function getPreferredMedia(body) {
|
|
|
129
131
|
if ("application/json" in body) return body["application/json"];
|
|
130
132
|
return Object.values(body)[0];
|
|
131
133
|
}
|
|
132
|
-
function
|
|
134
|
+
function toSampleInput(value) {
|
|
133
135
|
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
134
136
|
}
|
|
135
137
|
|
|
136
|
-
// src/
|
|
138
|
+
// src/utils/generate-input.ts
|
|
139
|
+
import { sample } from "openapi-sampler";
|
|
140
|
+
function generateInput(method, schema) {
|
|
141
|
+
return sample(schema, {
|
|
142
|
+
skipReadOnly: method !== "GET",
|
|
143
|
+
skipWriteOnly: method === "GET"
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/endpoint.ts
|
|
137
148
|
function createEndpoint(path, method, baseUrl) {
|
|
138
149
|
const params = [];
|
|
139
150
|
const responses = {};
|
|
@@ -161,33 +172,28 @@ function createEndpoint(path, method, baseUrl) {
|
|
|
161
172
|
let pathWithParameters = path;
|
|
162
173
|
const queryParams = new URLSearchParams();
|
|
163
174
|
for (const param of params) {
|
|
164
|
-
const value =
|
|
165
|
-
if (param.in === "query")
|
|
175
|
+
const value = generateInput(method.method, param.schema);
|
|
176
|
+
if (param.in === "query")
|
|
177
|
+
queryParams.append(param.name, toSampleInput(value));
|
|
166
178
|
if (param.in === "path")
|
|
167
179
|
pathWithParameters = pathWithParameters.replace(
|
|
168
180
|
`{${param.name}}`,
|
|
169
|
-
|
|
181
|
+
toSampleInput(value)
|
|
170
182
|
);
|
|
171
183
|
}
|
|
172
184
|
return {
|
|
173
185
|
url: new URL(pathWithParameters, baseUrl).toString(),
|
|
174
|
-
body: bodySchema ?
|
|
186
|
+
body: bodySchema ? generateInput(method.method, bodySchema) : void 0,
|
|
175
187
|
responses,
|
|
176
188
|
method: method.method,
|
|
177
189
|
parameters: params
|
|
178
190
|
};
|
|
179
191
|
}
|
|
180
|
-
function generateSample(method, schema) {
|
|
181
|
-
return sample(schema, {
|
|
182
|
-
skipReadOnly: method !== "GET",
|
|
183
|
-
skipWriteOnly: method === "GET"
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
192
|
|
|
187
|
-
// src/
|
|
193
|
+
// src/utils/generate-response.ts
|
|
188
194
|
function getExampleResponse(endpoint, code) {
|
|
189
195
|
if (code in endpoint.responses) {
|
|
190
|
-
const value =
|
|
196
|
+
const value = generateInput(
|
|
191
197
|
endpoint.method,
|
|
192
198
|
endpoint.responses[code].schema
|
|
193
199
|
);
|
|
@@ -195,38 +201,66 @@ function getExampleResponse(endpoint, code) {
|
|
|
195
201
|
}
|
|
196
202
|
}
|
|
197
203
|
|
|
198
|
-
// src/
|
|
199
|
-
import { compile } from "json-schema-to-typescript";
|
|
200
|
-
async function getTypescript(endpoint, code) {
|
|
201
|
-
if (code in endpoint.responses) {
|
|
202
|
-
return compile(endpoint.responses[code].schema, "Response", {
|
|
203
|
-
bannerComment: "",
|
|
204
|
-
additionalProperties: false,
|
|
205
|
-
format: true,
|
|
206
|
-
enableConstEnums: false
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// src/samples/curl.ts
|
|
204
|
+
// src/requests/curl.ts
|
|
212
205
|
function getSampleRequest(endpoint) {
|
|
213
206
|
const s = [];
|
|
214
207
|
s.push(`curl -X ${endpoint.method} "${endpoint.url}"`);
|
|
215
208
|
for (const param of endpoint.parameters) {
|
|
216
209
|
if (param.in === "header") {
|
|
217
|
-
const value =
|
|
218
|
-
const header = `${param.name}: ${
|
|
210
|
+
const value = generateInput(endpoint.method, param.schema);
|
|
211
|
+
const header = `${param.name}: ${toSampleInput(value)}`;
|
|
219
212
|
s.push(`-H "${header}"`);
|
|
220
213
|
}
|
|
221
214
|
if (param.in === "formData") {
|
|
222
215
|
console.log("Request example for form data is not supported");
|
|
223
216
|
}
|
|
224
217
|
}
|
|
225
|
-
if (endpoint.body) s.push(`-d '${
|
|
218
|
+
if (endpoint.body) s.push(`-d '${toSampleInput(endpoint.body)}'`);
|
|
226
219
|
return s.join(" \\\n ");
|
|
227
220
|
}
|
|
228
|
-
|
|
229
|
-
|
|
221
|
+
|
|
222
|
+
// src/requests/javascript.ts
|
|
223
|
+
function getSampleRequest2(endpoint) {
|
|
224
|
+
const s = [];
|
|
225
|
+
const options = /* @__PURE__ */ new Map();
|
|
226
|
+
const headers = {};
|
|
227
|
+
const formData = {};
|
|
228
|
+
for (const param of endpoint.parameters) {
|
|
229
|
+
if (param.in === "header") {
|
|
230
|
+
headers[param.name] = generateInput(endpoint.method, param.schema);
|
|
231
|
+
}
|
|
232
|
+
if (param.in === "formData") {
|
|
233
|
+
formData[param.name] = generateInput(endpoint.method, param.schema);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
options.set("method", JSON.stringify(endpoint.method));
|
|
237
|
+
if (Object.keys(headers).length > 0) {
|
|
238
|
+
options.set("headers", JSON.stringify(headers, void 0, 2));
|
|
239
|
+
}
|
|
240
|
+
if (Object.keys(formData).length > 0) {
|
|
241
|
+
s.push(`const formData = new FormData();`);
|
|
242
|
+
for (const [key, value] of Object.entries(formData))
|
|
243
|
+
s.push(`formData.set(${key}, ${JSON.stringify(value)}`);
|
|
244
|
+
options.set("body", "formData");
|
|
245
|
+
}
|
|
246
|
+
const optionsStr = Array.from(options.entries()).map(([k, v]) => ` ${k}: ${v}`).join(",\n");
|
|
247
|
+
s.push(`fetch(${JSON.stringify(endpoint.url)}, {
|
|
248
|
+
${optionsStr}
|
|
249
|
+
});`);
|
|
250
|
+
return s.join("\n\n");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/utils/get-typescript-schema.ts
|
|
254
|
+
import { compile } from "json-schema-to-typescript";
|
|
255
|
+
async function getTypescriptSchema(endpoint, code) {
|
|
256
|
+
if (code in endpoint.responses) {
|
|
257
|
+
return compile(endpoint.responses[code].schema, "Response", {
|
|
258
|
+
bannerComment: "",
|
|
259
|
+
additionalProperties: false,
|
|
260
|
+
format: true,
|
|
261
|
+
enableConstEnums: false
|
|
262
|
+
});
|
|
263
|
+
}
|
|
230
264
|
}
|
|
231
265
|
|
|
232
266
|
// src/render/schema.ts
|
|
@@ -344,18 +378,24 @@ function getSchemaType(schema) {
|
|
|
344
378
|
}
|
|
345
379
|
|
|
346
380
|
// src/render/operation.ts
|
|
347
|
-
async function renderOperation(path, method, ctx) {
|
|
381
|
+
async function renderOperation(path, method, ctx, noTitle = false) {
|
|
382
|
+
let level = 2;
|
|
383
|
+
const body = noRef(method.requestBody);
|
|
384
|
+
const security = method.security ?? ctx.document.security;
|
|
348
385
|
const info = [];
|
|
349
386
|
const example = [];
|
|
350
387
|
const title = method.summary ?? method.operationId;
|
|
351
|
-
if (title) info.push(
|
|
388
|
+
if (title && !noTitle) info.push(heading(level++, title));
|
|
352
389
|
if (method.description) info.push(p(method.description));
|
|
353
|
-
|
|
390
|
+
if (security) {
|
|
391
|
+
info.push(heading(level, "Authorization"));
|
|
392
|
+
info.push(getAuthSection(security, ctx));
|
|
393
|
+
}
|
|
354
394
|
if (body) {
|
|
355
395
|
const bodySchema = getPreferredMedia(body.content)?.schema;
|
|
356
396
|
if (!bodySchema) throw new Error();
|
|
357
397
|
info.push(
|
|
358
|
-
|
|
398
|
+
heading(level, `Request Body ${!body.required ? "(Optional)" : ""}`),
|
|
359
399
|
p(body.description),
|
|
360
400
|
schemaElement("body", noRef(bodySchema), {
|
|
361
401
|
parseObject: true,
|
|
@@ -399,11 +439,25 @@ async function renderOperation(path, method, ctx) {
|
|
|
399
439
|
parameterGroups.set(groupName, group);
|
|
400
440
|
}
|
|
401
441
|
for (const [group, parameters] of Array.from(parameterGroups.entries())) {
|
|
402
|
-
info.push(
|
|
442
|
+
info.push(heading(level, group), ...parameters);
|
|
403
443
|
}
|
|
404
444
|
info.push(getResponseTable(method));
|
|
405
445
|
example.push(
|
|
406
|
-
|
|
446
|
+
ctx.renderer.Requests(
|
|
447
|
+
["cURL", "JavaScript"],
|
|
448
|
+
[
|
|
449
|
+
ctx.renderer.Request({
|
|
450
|
+
name: "cURL",
|
|
451
|
+
code: getSampleRequest(endpoint),
|
|
452
|
+
language: "bash"
|
|
453
|
+
}),
|
|
454
|
+
ctx.renderer.Request({
|
|
455
|
+
name: "JavaScript",
|
|
456
|
+
code: getSampleRequest2(endpoint),
|
|
457
|
+
language: "js"
|
|
458
|
+
})
|
|
459
|
+
]
|
|
460
|
+
)
|
|
407
461
|
);
|
|
408
462
|
example.push(await getResponseTabs(endpoint, method, ctx));
|
|
409
463
|
return ctx.renderer.API([
|
|
@@ -411,6 +465,73 @@ async function renderOperation(path, method, ctx) {
|
|
|
411
465
|
ctx.renderer.APIExample(example)
|
|
412
466
|
]);
|
|
413
467
|
}
|
|
468
|
+
function getAuthSection(requirements, { document, renderer }) {
|
|
469
|
+
const info = [];
|
|
470
|
+
const schemas = document.components?.securitySchemes ?? {};
|
|
471
|
+
for (const requirement of requirements) {
|
|
472
|
+
if (info.length > 0) info.push(`---`);
|
|
473
|
+
for (const [name, scopes] of Object.entries(requirement)) {
|
|
474
|
+
if (!(name in schemas)) continue;
|
|
475
|
+
const schema = noRef(schemas[name]);
|
|
476
|
+
if (schema.type === "http") {
|
|
477
|
+
info.push(
|
|
478
|
+
renderer.Property(
|
|
479
|
+
{
|
|
480
|
+
name: "Authorization",
|
|
481
|
+
type: {
|
|
482
|
+
basic: "Basic <token>",
|
|
483
|
+
bearer: "Bearer <token>"
|
|
484
|
+
}[schema.scheme] ?? "<token>",
|
|
485
|
+
required: true
|
|
486
|
+
},
|
|
487
|
+
[p(schema.description), `In: \`header\``]
|
|
488
|
+
)
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
if (schema.type === "oauth2") {
|
|
492
|
+
info.push(
|
|
493
|
+
renderer.Property(
|
|
494
|
+
{
|
|
495
|
+
name: "Authorization",
|
|
496
|
+
type: "Bearer <token>",
|
|
497
|
+
required: true
|
|
498
|
+
},
|
|
499
|
+
[
|
|
500
|
+
p(schema.description),
|
|
501
|
+
`In: \`header\``,
|
|
502
|
+
`Scope: \`${scopes.length > 0 ? scopes.join(", ") : "none"}\``
|
|
503
|
+
]
|
|
504
|
+
)
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
if (schema.type === "apiKey") {
|
|
508
|
+
info.push(
|
|
509
|
+
renderer.Property(
|
|
510
|
+
{
|
|
511
|
+
name: schema.name,
|
|
512
|
+
type: "<token>",
|
|
513
|
+
required: true
|
|
514
|
+
},
|
|
515
|
+
[p(schema.description), `In: \`${schema.in}\``]
|
|
516
|
+
)
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
if (schema.type === "openIdConnect") {
|
|
520
|
+
info.push(
|
|
521
|
+
renderer.Property(
|
|
522
|
+
{
|
|
523
|
+
name: "OpenID Connect",
|
|
524
|
+
type: "<token>",
|
|
525
|
+
required: true
|
|
526
|
+
},
|
|
527
|
+
[p(schema.description)]
|
|
528
|
+
)
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return info.join("\n\n");
|
|
534
|
+
}
|
|
414
535
|
function getResponseTable(operation) {
|
|
415
536
|
const table = [];
|
|
416
537
|
table.push(`| Status code | Description |`);
|
|
@@ -425,7 +546,7 @@ async function getResponseTabs(endpoint, operation, { renderer }) {
|
|
|
425
546
|
const child = [];
|
|
426
547
|
for (const code of Object.keys(operation.responses)) {
|
|
427
548
|
const example = getExampleResponse(endpoint, code);
|
|
428
|
-
const ts = await
|
|
549
|
+
const ts = await getTypescriptSchema(endpoint, code);
|
|
429
550
|
const description = code in endpoint.responses ? endpoint.responses[code].schema.description : void 0;
|
|
430
551
|
if (example && ts) {
|
|
431
552
|
items.push(code);
|
|
@@ -467,6 +588,29 @@ async function generate(pathOrDocument, options = {}) {
|
|
|
467
588
|
options
|
|
468
589
|
);
|
|
469
590
|
}
|
|
591
|
+
async function generateOperations(pathOrDocument, options = {}) {
|
|
592
|
+
const document = await Parser.dereference(pathOrDocument);
|
|
593
|
+
const routes = buildRoutes(document).get("all") ?? [];
|
|
594
|
+
const ctx = getContext(document, options);
|
|
595
|
+
return await Promise.all(
|
|
596
|
+
routes.flatMap((route) => {
|
|
597
|
+
return route.methods.map(async (method) => {
|
|
598
|
+
const content = renderPage(
|
|
599
|
+
method.summary ?? method.method,
|
|
600
|
+
method.description,
|
|
601
|
+
[await renderOperation(route.path, method, ctx, true)],
|
|
602
|
+
options
|
|
603
|
+
);
|
|
604
|
+
if (!method.operationId)
|
|
605
|
+
throw new Error("Operation ID is required for generating docs.");
|
|
606
|
+
return {
|
|
607
|
+
id: method.operationId,
|
|
608
|
+
content
|
|
609
|
+
};
|
|
610
|
+
});
|
|
611
|
+
})
|
|
612
|
+
);
|
|
613
|
+
}
|
|
470
614
|
async function generateTags(pathOrDocument, options = {}) {
|
|
471
615
|
const document = await Parser.dereference(pathOrDocument);
|
|
472
616
|
const tags = Array.from(buildRoutes(document).entries());
|
|
@@ -489,6 +633,7 @@ async function generateTags(pathOrDocument, options = {}) {
|
|
|
489
633
|
}
|
|
490
634
|
function getContext(document, options) {
|
|
491
635
|
return {
|
|
636
|
+
document,
|
|
492
637
|
renderer: {
|
|
493
638
|
...defaultRenderer,
|
|
494
639
|
...options.renderer
|
|
@@ -522,10 +667,24 @@ async function generateFiles({
|
|
|
522
667
|
console.log(`Generated: ${outPath}`);
|
|
523
668
|
return;
|
|
524
669
|
}
|
|
670
|
+
if (per === "operation") {
|
|
671
|
+
const results2 = await generateOperations(path, options);
|
|
672
|
+
await Promise.all(
|
|
673
|
+
results2.map(async (result) => {
|
|
674
|
+
const outPath = join(
|
|
675
|
+
outputDir,
|
|
676
|
+
filename,
|
|
677
|
+
`${getName(result.id)}.mdx`
|
|
678
|
+
);
|
|
679
|
+
await write(outPath, result.content);
|
|
680
|
+
console.log(`Generated: ${outPath}`);
|
|
681
|
+
})
|
|
682
|
+
);
|
|
683
|
+
}
|
|
525
684
|
const results = await generateTags(path, options);
|
|
526
685
|
for (const result of results) {
|
|
527
686
|
let tagName = result.tag;
|
|
528
|
-
tagName = nameFn?.("tag", tagName) ?? tagName
|
|
687
|
+
tagName = nameFn?.("tag", tagName) ?? getName(tagName);
|
|
529
688
|
const outPath = join(outputDir, filename, `${tagName}.mdx`);
|
|
530
689
|
await write(outPath, result.content);
|
|
531
690
|
console.log(`Generated: ${outPath}`);
|
|
@@ -533,6 +692,12 @@ async function generateFiles({
|
|
|
533
692
|
})
|
|
534
693
|
);
|
|
535
694
|
}
|
|
695
|
+
function getName(s) {
|
|
696
|
+
return s.replace(
|
|
697
|
+
/[A-Z]/g,
|
|
698
|
+
(match, idx) => idx === 0 ? match : `-${match.toLowerCase()}`
|
|
699
|
+
).replace(/\s+/g, "-").toLowerCase();
|
|
700
|
+
}
|
|
536
701
|
async function write(path, content) {
|
|
537
702
|
await mkdir(dirname(path), { recursive: true });
|
|
538
703
|
await writeFile(path, content);
|
|
@@ -542,5 +707,6 @@ export {
|
|
|
542
707
|
defaultRenderer,
|
|
543
708
|
generate,
|
|
544
709
|
generateFiles,
|
|
710
|
+
generateOperations,
|
|
545
711
|
generateTags
|
|
546
712
|
};
|