fumadocs-openapi 4.4.1 → 5.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 +113 -36
- package/dist/index.js +252 -1030
- package/dist/server/index.d.ts +211 -0
- package/dist/server/index.js +1093 -0
- package/dist/ui/client-client-CbQJObP6.js +91 -0
- package/dist/ui/index.d.ts +87 -43
- package/dist/ui/index.js +128 -185
- package/dist/ui/playground-client-W7yhm4ZD.js +1033 -0
- package/package.json +15 -10
- package/dist/chunk-N4P4W4VJ.js +0 -61
- package/dist/chunk-UG2WFM5D.js +0 -70
- package/dist/playground-GIGTWHCL.js +0 -1065
- package/dist/playground-vSsfCaVw.d.ts +0 -56
- package/dist/source/index.d.ts +0 -10
- package/dist/source/index.js +0 -39
package/dist/index.js
CHANGED
|
@@ -1,1068 +1,290 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import Parser from '@apidevtools/json-schema-ref-parser';
|
|
2
|
+
import Slugger from 'github-slugger';
|
|
3
|
+
import { dump } from 'js-yaml';
|
|
4
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { join, parse, dirname } from 'node:path';
|
|
6
|
+
import fg from 'fast-glob';
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
};
|
|
8
|
+
function createMethod(method, operation) {
|
|
9
|
+
return {
|
|
10
|
+
...operation,
|
|
11
|
+
parameters: operation.parameters ?? [],
|
|
12
|
+
method: method.toUpperCase()
|
|
13
|
+
};
|
|
47
14
|
}
|
|
48
15
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
16
|
+
const methodKeys = [
|
|
17
|
+
'get',
|
|
18
|
+
'post',
|
|
19
|
+
'patch',
|
|
20
|
+
'delete',
|
|
21
|
+
'head',
|
|
22
|
+
'put'
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Build the route information of tags, use `.get('all')` to get all entries
|
|
26
|
+
*/ function buildRoutes(document) {
|
|
27
|
+
const map = new Map();
|
|
28
|
+
for (const [path, value] of Object.entries(document.paths)){
|
|
29
|
+
if (!value) continue;
|
|
30
|
+
const methodMap = new Map();
|
|
31
|
+
for (const methodKey of methodKeys){
|
|
32
|
+
const operation = value[methodKey];
|
|
33
|
+
if (!operation) continue;
|
|
34
|
+
const info = createMethod(methodKey, operation);
|
|
35
|
+
const tags = operation.tags ?? [];
|
|
36
|
+
for (const tag of [
|
|
37
|
+
...tags,
|
|
38
|
+
'all'
|
|
39
|
+
]){
|
|
40
|
+
const list = methodMap.get(tag) ?? [];
|
|
41
|
+
list.push(info);
|
|
42
|
+
methodMap.set(tag, list);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const [tag, methods] of methodMap.entries()){
|
|
46
|
+
const list = map.get(tag) ?? [];
|
|
47
|
+
list.push({
|
|
48
|
+
...value,
|
|
49
|
+
path,
|
|
50
|
+
methods
|
|
51
|
+
});
|
|
52
|
+
map.set(tag, list);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return map;
|
|
77
56
|
}
|
|
78
57
|
|
|
79
|
-
// src/render/renderer.ts
|
|
80
|
-
var defaultRenderer = {
|
|
81
|
-
Root: (props, child) => createElement("Root", props, ...child),
|
|
82
|
-
API: (child) => createElement("API", {}, ...child),
|
|
83
|
-
APIInfo: (props, child) => createElement("APIInfo", props, ...child),
|
|
84
|
-
APIExample: (child) => createElement("APIExample", {}, ...child),
|
|
85
|
-
Responses: (props, child) => createElement("Responses", props, ...child),
|
|
86
|
-
Response: (props, child) => createElement("Response", props, ...child),
|
|
87
|
-
ResponseTypes: (child) => createElement("ResponseTypes", {}, ...child),
|
|
88
|
-
ExampleResponse: (json) => createElement("ExampleResponse", {}, codeblock({ language: "json" }, json)),
|
|
89
|
-
TypeScriptResponse: (code) => createElement(
|
|
90
|
-
"TypeScriptResponse",
|
|
91
|
-
{},
|
|
92
|
-
codeblock({ language: "ts" }, code)
|
|
93
|
-
),
|
|
94
|
-
Property: (props, child) => createElement("Property", 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)),
|
|
98
|
-
APIPlayground: (props) => createElement("APIPlayground", props)
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
// src/utils/generate-document.ts
|
|
102
58
|
function generateDocument(content, options, frontmatter) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
{
|
|
119
|
-
names: Object.keys(defaultRenderer),
|
|
120
|
-
from: "fumadocs-openapi/ui"
|
|
59
|
+
const out = [];
|
|
60
|
+
const banner = dump({
|
|
61
|
+
title: frontmatter.title,
|
|
62
|
+
description: frontmatter.description,
|
|
63
|
+
full: true,
|
|
64
|
+
...frontmatter.context.type === 'operation' ? {
|
|
65
|
+
method: frontmatter.context.endpoint.method,
|
|
66
|
+
route: frontmatter.context.route.path
|
|
67
|
+
} : undefined,
|
|
68
|
+
...options.frontmatter?.(frontmatter.title, frontmatter.description, frontmatter.context)
|
|
69
|
+
}).trim();
|
|
70
|
+
if (banner.length > 0) out.push(`---\n${banner}\n---`);
|
|
71
|
+
const imports = options.imports?.map((item)=>`import { ${item.names.join(', ')} } from ${JSON.stringify(item.from)};`).join('\n');
|
|
72
|
+
if (imports) {
|
|
73
|
+
out.push(imports);
|
|
121
74
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
).join("\n");
|
|
125
|
-
return `---
|
|
126
|
-
${banner}
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
${finalImports}
|
|
130
|
-
|
|
131
|
-
${content}`;
|
|
75
|
+
out.push(content);
|
|
76
|
+
return out.join('\n\n');
|
|
132
77
|
}
|
|
133
78
|
|
|
134
|
-
// src/utils/id-to-title.ts
|
|
135
79
|
function idToTitle(id) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return result.join("");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// src/endpoint.ts
|
|
147
|
-
import { sample as sample2 } from "openapi-sampler";
|
|
148
|
-
|
|
149
|
-
// src/utils/schema.ts
|
|
150
|
-
function noRef(v) {
|
|
151
|
-
return v;
|
|
152
|
-
}
|
|
153
|
-
function getPreferredMedia(body) {
|
|
154
|
-
const type = getPreferredType(body);
|
|
155
|
-
if (type) return body[type];
|
|
156
|
-
}
|
|
157
|
-
function getPreferredType(body) {
|
|
158
|
-
if ("application/json" in body) return "application/json";
|
|
159
|
-
return Object.keys(body)[0];
|
|
160
|
-
}
|
|
161
|
-
function toSampleInput(value) {
|
|
162
|
-
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// src/utils/generate-input.ts
|
|
166
|
-
import { sample } from "openapi-sampler";
|
|
167
|
-
function generateInput(method, schema) {
|
|
168
|
-
return sample(schema, {
|
|
169
|
-
skipReadOnly: method !== "GET",
|
|
170
|
-
skipWriteOnly: method === "GET"
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// src/endpoint.ts
|
|
175
|
-
function createEndpoint(path, method, baseUrl) {
|
|
176
|
-
const params = [];
|
|
177
|
-
const responses = {};
|
|
178
|
-
for (const param of method.parameters) {
|
|
179
|
-
if (param.schema) {
|
|
180
|
-
params.push({
|
|
181
|
-
name: param.name,
|
|
182
|
-
in: param.in,
|
|
183
|
-
schema: noRef(param.schema),
|
|
184
|
-
sample: param.example ?? sample2(param.schema)
|
|
185
|
-
});
|
|
186
|
-
} else if (param.content) {
|
|
187
|
-
const key = getPreferredType(param.content);
|
|
188
|
-
const content = key ? param.content[key] : void 0;
|
|
189
|
-
if (!key || !content?.schema)
|
|
190
|
-
throw new Error(
|
|
191
|
-
`Cannot find parameter schema for ${param.name} in ${path} ${method.method}`
|
|
192
|
-
);
|
|
193
|
-
params.push({
|
|
194
|
-
name: param.name,
|
|
195
|
-
in: param.in,
|
|
196
|
-
schema: noRef(content.schema),
|
|
197
|
-
sample: content.example ?? param.example ?? sample2(content.schema)
|
|
198
|
-
});
|
|
80
|
+
let result = [];
|
|
81
|
+
for (const c of id){
|
|
82
|
+
if (result.length === 0) result.push(c.toLocaleUpperCase());
|
|
83
|
+
else if (c === '.') result = [];
|
|
84
|
+
else if (/^[A-Z]$/.test(c) && result.at(-1) !== ' ') result.push(' ', c);
|
|
85
|
+
else if (c === '-') result.push(' ');
|
|
86
|
+
else result.push(c);
|
|
199
87
|
}
|
|
200
|
-
|
|
201
|
-
let bodyOutput;
|
|
202
|
-
if (method.requestBody) {
|
|
203
|
-
const body = noRef(method.requestBody).content;
|
|
204
|
-
const type = getPreferredType(body);
|
|
205
|
-
const schema = type ? noRef(body[type].schema) : void 0;
|
|
206
|
-
if (!type || !schema)
|
|
207
|
-
throw new Error(`Cannot find body schema for ${path} ${method.method}`);
|
|
208
|
-
bodyOutput = {
|
|
209
|
-
schema,
|
|
210
|
-
mediaType: type,
|
|
211
|
-
sample: body[type].example ?? generateInput(method.method, schema)
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
for (const [code, value] of Object.entries(method.responses)) {
|
|
215
|
-
const mediaTypes = noRef(value).content ?? {};
|
|
216
|
-
const responseSchema = noRef(getPreferredMedia(mediaTypes)?.schema);
|
|
217
|
-
if (!responseSchema) continue;
|
|
218
|
-
responses[code] = {
|
|
219
|
-
schema: responseSchema
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
let pathWithParameters = path;
|
|
223
|
-
const queryParams = new URLSearchParams();
|
|
224
|
-
for (const param of params) {
|
|
225
|
-
const value = generateInput(method.method, param.schema);
|
|
226
|
-
if (param.in === "query")
|
|
227
|
-
queryParams.append(param.name, toSampleInput(value));
|
|
228
|
-
if (param.in === "path")
|
|
229
|
-
pathWithParameters = pathWithParameters.replace(
|
|
230
|
-
`{${param.name}}`,
|
|
231
|
-
toSampleInput(value)
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
if (queryParams.size > 0)
|
|
235
|
-
pathWithParameters = `${pathWithParameters}?${queryParams.toString()}`;
|
|
236
|
-
return {
|
|
237
|
-
url: new URL(`${baseUrl}${pathWithParameters}`).toString(),
|
|
238
|
-
body: bodyOutput,
|
|
239
|
-
responses,
|
|
240
|
-
method: method.method,
|
|
241
|
-
parameters: params
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// src/utils/generate-response.ts
|
|
246
|
-
function getExampleResponse(endpoint, code) {
|
|
247
|
-
if (code in endpoint.responses) {
|
|
248
|
-
const value = generateInput(
|
|
249
|
-
endpoint.method,
|
|
250
|
-
endpoint.responses[code].schema
|
|
251
|
-
);
|
|
252
|
-
return JSON.stringify(value, null, 2);
|
|
253
|
-
}
|
|
88
|
+
return result.join('');
|
|
254
89
|
}
|
|
255
90
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
console.warn("Curl sample with form data body isn't supported.");
|
|
268
|
-
if (endpoint.body) s.push(`-d '${toSampleInput(endpoint.body.sample)}'`);
|
|
269
|
-
return s.join(" \\\n ");
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// src/requests/javascript.ts
|
|
273
|
-
function getSampleRequest2(endpoint) {
|
|
274
|
-
const s = [];
|
|
275
|
-
const options = /* @__PURE__ */ new Map();
|
|
276
|
-
const headers = {};
|
|
277
|
-
for (const param of endpoint.parameters) {
|
|
278
|
-
if (param.in === "header") {
|
|
279
|
-
headers[param.name] = generateInput(endpoint.method, param.schema);
|
|
91
|
+
async function generateAll(pathOrDocument, options = {}) {
|
|
92
|
+
const document = await Parser.dereference(pathOrDocument);
|
|
93
|
+
const routes = buildRoutes(document).get('all') ?? [];
|
|
94
|
+
const operations = [];
|
|
95
|
+
for (const route of routes){
|
|
96
|
+
for (const method of route.methods){
|
|
97
|
+
operations.push({
|
|
98
|
+
method: method.method.toLowerCase(),
|
|
99
|
+
path: route.path
|
|
100
|
+
});
|
|
101
|
+
}
|
|
280
102
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
} else if (endpoint.body) {
|
|
292
|
-
options.set(
|
|
293
|
-
"body",
|
|
294
|
-
`JSON.stringify(${JSON.stringify(endpoint.body.sample, null, 2).split("\n").map((v, i) => i > 0 ? ` ${v}` : v).join("\n")})`
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
const optionsStr = Array.from(options.entries()).map(([k, v]) => ` ${k}: ${v}`).join(",\n");
|
|
298
|
-
s.push(`fetch(${JSON.stringify(endpoint.url)}, {
|
|
299
|
-
${optionsStr}
|
|
300
|
-
});`);
|
|
301
|
-
return s.join("\n\n");
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// src/utils/get-typescript-schema.ts
|
|
305
|
-
import { compile } from "json-schema-to-typescript";
|
|
306
|
-
async function getTypescriptSchema(endpoint, code) {
|
|
307
|
-
if (code in endpoint.responses) {
|
|
308
|
-
return compile(endpoint.responses[code].schema, "Response", {
|
|
309
|
-
bannerComment: "",
|
|
310
|
-
additionalProperties: false,
|
|
311
|
-
format: true,
|
|
312
|
-
enableConstEnums: false
|
|
103
|
+
return generateDocument(pageContent(document, {
|
|
104
|
+
operations,
|
|
105
|
+
hasHead: true
|
|
106
|
+
}), options, {
|
|
107
|
+
title: document.info.title,
|
|
108
|
+
description: document.info.description,
|
|
109
|
+
context: {
|
|
110
|
+
type: 'file',
|
|
111
|
+
routes
|
|
112
|
+
}
|
|
313
113
|
});
|
|
314
|
-
}
|
|
315
114
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
115
|
+
async function generateOperations(pathOrDocument, options = {}) {
|
|
116
|
+
const document = await Parser.dereference(pathOrDocument);
|
|
117
|
+
const routes = buildRoutes(document).get('all') ?? [];
|
|
118
|
+
return routes.flatMap((route)=>{
|
|
119
|
+
return route.methods.map((method)=>{
|
|
120
|
+
if (!method.operationId) throw new Error('Operation ID is required for generating docs.');
|
|
121
|
+
const content = generateDocument(pageContent(document, {
|
|
122
|
+
operations: [
|
|
123
|
+
{
|
|
124
|
+
path: route.path,
|
|
125
|
+
method: method.method.toLowerCase()
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
hasHead: false
|
|
129
|
+
}), options, {
|
|
130
|
+
title: method.summary ?? idToTitle(method.operationId),
|
|
131
|
+
description: method.description,
|
|
132
|
+
context: {
|
|
133
|
+
type: 'operation',
|
|
134
|
+
endpoint: method,
|
|
135
|
+
route
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
content,
|
|
140
|
+
route,
|
|
141
|
+
method
|
|
142
|
+
};
|
|
143
|
+
});
|
|
327
144
|
});
|
|
328
|
-
}
|
|
329
|
-
return results;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// src/render/playground.ts
|
|
333
|
-
function renderPlayground(path, method, ctx) {
|
|
334
|
-
let currentId = 0;
|
|
335
|
-
const bodyContent = noRef(method.requestBody)?.content;
|
|
336
|
-
const mediaType = bodyContent ? getPreferredType(bodyContent) : void 0;
|
|
337
|
-
const context = {
|
|
338
|
-
allowFile: mediaType === "multipart/form-data",
|
|
339
|
-
schema: {},
|
|
340
|
-
nextId() {
|
|
341
|
-
return String(currentId++);
|
|
342
|
-
},
|
|
343
|
-
registered: /* @__PURE__ */ new WeakMap()
|
|
344
|
-
};
|
|
345
|
-
return ctx.renderer.APIPlayground({
|
|
346
|
-
authorization: getAuthorizationField(method, ctx),
|
|
347
|
-
method: method.method,
|
|
348
|
-
route: path,
|
|
349
|
-
bodyType: mediaType === "multipart/form-data" ? "form-data" : "json",
|
|
350
|
-
path: method.parameters.filter((v) => v.in === "path").map((v) => parameterToField(v, context)),
|
|
351
|
-
query: method.parameters.filter((v) => v.in === "query").map((v) => parameterToField(v, context)),
|
|
352
|
-
header: method.parameters.filter((v) => v.in === "header").map((v) => parameterToField(v, context)),
|
|
353
|
-
body: bodyContent && mediaType && bodyContent[mediaType].schema ? toSchema(noRef(bodyContent[mediaType].schema), true, context) : void 0,
|
|
354
|
-
schemas: context.schema
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
function getAuthorizationField(method, ctx) {
|
|
358
|
-
const security = method.security ?? ctx.document.security ?? [];
|
|
359
|
-
if (security.length === 0) return;
|
|
360
|
-
const singular = security.find(
|
|
361
|
-
(requirements) => Object.keys(requirements).length === 1
|
|
362
|
-
);
|
|
363
|
-
if (!singular) return;
|
|
364
|
-
const scheme = getScheme(singular, ctx.document)[0];
|
|
365
|
-
return {
|
|
366
|
-
type: "string",
|
|
367
|
-
name: "Authorization",
|
|
368
|
-
defaultValue: scheme.type === "oauth2" || scheme.type === "http" && scheme.scheme === "bearer" ? "Bearer" : "Basic",
|
|
369
|
-
isRequired: security.every(
|
|
370
|
-
(requirements) => Object.keys(requirements).length > 0
|
|
371
|
-
),
|
|
372
|
-
description: "The Authorization access token"
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
function getIdFromSchema(schema, required, ctx) {
|
|
376
|
-
const registered = ctx.registered.get(schema);
|
|
377
|
-
if (registered === void 0) {
|
|
378
|
-
const id = ctx.nextId();
|
|
379
|
-
ctx.registered.set(schema, id);
|
|
380
|
-
ctx.schema[id] = toSchema(schema, required, ctx);
|
|
381
|
-
return id;
|
|
382
|
-
}
|
|
383
|
-
return registered;
|
|
384
145
|
}
|
|
385
|
-
function
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
properties[key] = toReference(
|
|
415
|
-
noRef(prop),
|
|
416
|
-
schema.required?.includes(key) ?? false,
|
|
417
|
-
ctx
|
|
418
|
-
);
|
|
419
|
-
});
|
|
420
|
-
schema.allOf?.forEach((c) => {
|
|
421
|
-
const field = toSchema(noRef(c), true, ctx);
|
|
422
|
-
if (field.type === "object") Object.assign(properties, field.properties);
|
|
146
|
+
async function generateTags(pathOrDocument, options = {}) {
|
|
147
|
+
const document = await Parser.dereference(pathOrDocument);
|
|
148
|
+
const tags = Array.from(buildRoutes(document).entries());
|
|
149
|
+
return tags.filter(([tag])=>tag !== 'all').map(([tag, routes])=>{
|
|
150
|
+
const info = document.tags?.find((t)=>t.name === tag);
|
|
151
|
+
const operations = [];
|
|
152
|
+
for (const route of routes){
|
|
153
|
+
for (const method of route.methods){
|
|
154
|
+
operations.push({
|
|
155
|
+
method: method.method.toLowerCase(),
|
|
156
|
+
path: route.path
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
tag,
|
|
162
|
+
content: generateDocument(pageContent(document, {
|
|
163
|
+
operations,
|
|
164
|
+
hasHead: true
|
|
165
|
+
}), options, {
|
|
166
|
+
title: idToTitle(tag),
|
|
167
|
+
description: info?.description,
|
|
168
|
+
context: {
|
|
169
|
+
type: 'tag',
|
|
170
|
+
tag: info,
|
|
171
|
+
routes
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
};
|
|
423
175
|
});
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
} else {
|
|
433
|
-
additionalProperties = additional;
|
|
434
|
-
}
|
|
435
|
-
return {
|
|
436
|
-
type: "object",
|
|
437
|
-
isRequired: required,
|
|
438
|
-
description: schema.description ?? schema.title,
|
|
439
|
-
properties,
|
|
440
|
-
additionalProperties
|
|
176
|
+
}
|
|
177
|
+
function pageContent(doc, props) {
|
|
178
|
+
const slugger = new Slugger();
|
|
179
|
+
const toc = [];
|
|
180
|
+
const structuredData = {
|
|
181
|
+
headings: [],
|
|
182
|
+
contents: []
|
|
441
183
|
};
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
184
|
+
for (const item of props.operations){
|
|
185
|
+
const operation = doc.paths[item.path]?.[item.method];
|
|
186
|
+
if (!operation) continue;
|
|
187
|
+
if (props.hasHead && operation.operationId) {
|
|
188
|
+
const title = operation.summary ?? (operation.operationId ? idToTitle(operation.operationId) : item.path);
|
|
189
|
+
const id = slugger.slug(title);
|
|
190
|
+
toc.push({
|
|
191
|
+
depth: 2,
|
|
192
|
+
title,
|
|
193
|
+
url: `#${id}`
|
|
194
|
+
});
|
|
195
|
+
structuredData.headings.push({
|
|
196
|
+
content: title,
|
|
197
|
+
id
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (operation.description) structuredData.contents.push({
|
|
201
|
+
content: operation.description,
|
|
202
|
+
heading: structuredData.headings.at(-1)?.id
|
|
203
|
+
});
|
|
460
204
|
}
|
|
461
|
-
return {
|
|
462
|
-
type: "null",
|
|
463
|
-
isRequired: false
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
if (ctx.allowFile && schema.type === "string" && schema.format === "binary") {
|
|
467
|
-
return {
|
|
468
|
-
type: "file",
|
|
469
|
-
isRequired: required,
|
|
470
|
-
description: schema.description ?? schema.title
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
return {
|
|
474
|
-
type: schema.type === "integer" ? "number" : schema.type,
|
|
475
|
-
defaultValue: schema.example ?? "",
|
|
476
|
-
isRequired: required,
|
|
477
|
-
description: schema.description ?? schema.title
|
|
478
|
-
};
|
|
479
|
-
}
|
|
205
|
+
return `<APIPage operations={${JSON.stringify(props.operations)}} hasHead={${JSON.stringify(props.hasHead)}} />
|
|
480
206
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
maximum: "Maximum",
|
|
487
|
-
minLength: "Minimum length",
|
|
488
|
-
maxLength: "Maximum length",
|
|
489
|
-
pattern: "Pattern",
|
|
490
|
-
format: "Format"
|
|
491
|
-
};
|
|
492
|
-
function isObject(schema) {
|
|
493
|
-
return schema.type === "object" || schema.properties !== void 0 || schema.additionalProperties !== void 0;
|
|
494
|
-
}
|
|
495
|
-
function schemaElement(name, schema, ctx) {
|
|
496
|
-
return render(name, schema, {
|
|
497
|
-
...ctx,
|
|
498
|
-
stack: []
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
function render(name, schema, ctx) {
|
|
502
|
-
if (schema.readOnly && !ctx.readOnly) return "";
|
|
503
|
-
if (schema.writeOnly && !ctx.writeOnly) return "";
|
|
504
|
-
const { renderer } = ctx.render;
|
|
505
|
-
const child = [];
|
|
506
|
-
function field(key, value) {
|
|
507
|
-
child.push(span(`${key}: \`${value}\``));
|
|
508
|
-
}
|
|
509
|
-
if (isObject(schema) && ctx.parseObject) {
|
|
510
|
-
const { additionalProperties, properties } = schema;
|
|
511
|
-
if (additionalProperties === true) {
|
|
512
|
-
child.push(
|
|
513
|
-
renderer.Property(
|
|
514
|
-
{
|
|
515
|
-
name: "[key: string]",
|
|
516
|
-
type: "any"
|
|
517
|
-
},
|
|
518
|
-
[]
|
|
519
|
-
)
|
|
520
|
-
);
|
|
521
|
-
} else if (additionalProperties) {
|
|
522
|
-
child.push(
|
|
523
|
-
render("[key: string]", noRef(additionalProperties), {
|
|
524
|
-
...ctx,
|
|
525
|
-
required: false,
|
|
526
|
-
parseObject: false
|
|
527
|
-
})
|
|
528
|
-
);
|
|
207
|
+
export function startup() {
|
|
208
|
+
if (toc) {
|
|
209
|
+
// toc might be immutable
|
|
210
|
+
while (toc.length > 0) toc.pop()
|
|
211
|
+
toc.push(...${JSON.stringify(toc)})
|
|
529
212
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
required: schema.required?.includes(key) ?? false,
|
|
535
|
-
parseObject: false
|
|
536
|
-
})
|
|
537
|
-
);
|
|
538
|
-
});
|
|
539
|
-
return child.join("\n\n");
|
|
540
|
-
}
|
|
541
|
-
child.push(p(schema.description));
|
|
542
|
-
for (const [key, value] of Object.entries(keys)) {
|
|
543
|
-
if (key in schema) {
|
|
544
|
-
field(value, JSON.stringify(schema[key]));
|
|
213
|
+
|
|
214
|
+
if (structuredData) {
|
|
215
|
+
structuredData.headings = ${JSON.stringify(structuredData.headings)}
|
|
216
|
+
structuredData.contents = ${JSON.stringify(structuredData.contents)}
|
|
545
217
|
}
|
|
546
|
-
}
|
|
547
|
-
if (schema.enum) {
|
|
548
|
-
field(
|
|
549
|
-
"Value in",
|
|
550
|
-
schema.enum.map((value) => JSON.stringify(value)).join(" | ")
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
if (isObject(schema) && !ctx.parseObject) {
|
|
554
|
-
child.push(
|
|
555
|
-
renderer.ObjectCollapsible({ name }, [
|
|
556
|
-
render(name, schema, {
|
|
557
|
-
...ctx,
|
|
558
|
-
parseObject: true,
|
|
559
|
-
required: false
|
|
560
|
-
})
|
|
561
|
-
])
|
|
562
|
-
);
|
|
563
|
-
} else if (schema.allOf) {
|
|
564
|
-
child.push(
|
|
565
|
-
renderer.ObjectCollapsible({ name }, [
|
|
566
|
-
render(name, combineSchema(schema.allOf.map(noRef)), {
|
|
567
|
-
...ctx,
|
|
568
|
-
parseObject: true,
|
|
569
|
-
required: false
|
|
570
|
-
})
|
|
571
|
-
])
|
|
572
|
-
);
|
|
573
|
-
} else {
|
|
574
|
-
const mentionedObjectTypes = [
|
|
575
|
-
...schema.anyOf ?? schema.oneOf ?? [],
|
|
576
|
-
...schema.not ? [schema.not] : [],
|
|
577
|
-
...schema.type === "array" ? [schema.items] : []
|
|
578
|
-
].map(noRef).filter((s) => isComplexType(s) && !ctx.stack.includes(s));
|
|
579
|
-
ctx.stack.push(schema);
|
|
580
|
-
child.push(
|
|
581
|
-
...mentionedObjectTypes.map(
|
|
582
|
-
(s, idx) => renderer.ObjectCollapsible(
|
|
583
|
-
{ name: s.title ?? `Object ${(idx + 1).toString()}` },
|
|
584
|
-
[
|
|
585
|
-
render("element", noRef(s), {
|
|
586
|
-
...ctx,
|
|
587
|
-
parseObject: true,
|
|
588
|
-
required: false
|
|
589
|
-
})
|
|
590
|
-
]
|
|
591
|
-
)
|
|
592
|
-
)
|
|
593
|
-
);
|
|
594
|
-
ctx.stack.pop();
|
|
595
|
-
}
|
|
596
|
-
return renderer.Property(
|
|
597
|
-
{
|
|
598
|
-
name,
|
|
599
|
-
type: getSchemaType(schema, ctx),
|
|
600
|
-
required: ctx.required,
|
|
601
|
-
deprecated: schema.deprecated
|
|
602
|
-
},
|
|
603
|
-
child
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
function combineSchema(schema) {
|
|
607
|
-
const result = {
|
|
608
|
-
type: "object"
|
|
609
|
-
};
|
|
610
|
-
function add(s) {
|
|
611
|
-
result.properties ??= {};
|
|
612
|
-
if (s.properties) {
|
|
613
|
-
Object.assign(result.properties, s.properties);
|
|
614
|
-
}
|
|
615
|
-
result.additionalProperties ??= {};
|
|
616
|
-
if (s.additionalProperties === true) {
|
|
617
|
-
result.additionalProperties = true;
|
|
618
|
-
} else if (s.additionalProperties && typeof result.additionalProperties !== "boolean") {
|
|
619
|
-
Object.assign(result.additionalProperties, s.additionalProperties);
|
|
620
|
-
}
|
|
621
|
-
if (s.allOf) {
|
|
622
|
-
add(combineSchema(s.allOf.map(noRef)));
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
schema.forEach(add);
|
|
626
|
-
return result;
|
|
627
|
-
}
|
|
628
|
-
function isComplexType(schema) {
|
|
629
|
-
if (schema.anyOf ?? schema.oneOf ?? schema.allOf) return true;
|
|
630
|
-
return isObject(schema) || schema.type === "array";
|
|
631
|
-
}
|
|
632
|
-
function getSchemaType(schema, ctx) {
|
|
633
|
-
if (schema.nullable) {
|
|
634
|
-
const type = getSchemaType({ ...schema, nullable: false }, ctx);
|
|
635
|
-
return type === "unknown" ? "null" : `${type} | null`;
|
|
636
|
-
}
|
|
637
|
-
if (schema.title) return schema.title;
|
|
638
|
-
if (schema.type === "array")
|
|
639
|
-
return `array<${getSchemaType(noRef(schema.items), ctx)}>`;
|
|
640
|
-
if (schema.oneOf)
|
|
641
|
-
return schema.oneOf.map((one) => getSchemaType(noRef(one), ctx)).join(" | ");
|
|
642
|
-
if (schema.allOf)
|
|
643
|
-
return schema.allOf.map((one) => getSchemaType(noRef(one), ctx)).join(" & ");
|
|
644
|
-
if (schema.not) return `not ${getSchemaType(noRef(schema.not), ctx)}`;
|
|
645
|
-
if (schema.anyOf) {
|
|
646
|
-
return `Any properties in ${schema.anyOf.map((one) => getSchemaType(noRef(one), ctx)).join(", ")}`;
|
|
647
|
-
}
|
|
648
|
-
if (schema.type === "string" && schema.format === "binary" && ctx.allowFile)
|
|
649
|
-
return "File";
|
|
650
|
-
if (schema.type) return schema.type;
|
|
651
|
-
if (isObject(schema)) return "object";
|
|
652
|
-
return "unknown";
|
|
653
218
|
}
|
|
654
219
|
|
|
655
|
-
|
|
656
|
-
async function renderOperation(path, method, ctx, hasHead = true) {
|
|
657
|
-
let level = 2;
|
|
658
|
-
const body = noRef(method.requestBody);
|
|
659
|
-
const security = method.security ?? ctx.document.security;
|
|
660
|
-
const info = [];
|
|
661
|
-
const example = [];
|
|
662
|
-
if (hasHead) {
|
|
663
|
-
info.push(
|
|
664
|
-
heading(
|
|
665
|
-
level,
|
|
666
|
-
method.summary ?? (method.operationId ? idToTitle(method.operationId) : path)
|
|
667
|
-
)
|
|
668
|
-
);
|
|
669
|
-
level++;
|
|
670
|
-
if (method.description) info.push(p(method.description));
|
|
671
|
-
}
|
|
672
|
-
info.push(renderPlayground(path, method, ctx));
|
|
673
|
-
if (security) {
|
|
674
|
-
info.push(heading(level, "Authorization"));
|
|
675
|
-
info.push(getAuthSection(security, ctx));
|
|
676
|
-
}
|
|
677
|
-
if (body) {
|
|
678
|
-
const type = getPreferredType(body.content);
|
|
679
|
-
if (!type)
|
|
680
|
-
throw new Error(`No supported media type for body content: ${path}`);
|
|
681
|
-
info.push(
|
|
682
|
-
heading(level, `Request Body ${!body.required ? "(Optional)" : ""}`),
|
|
683
|
-
p(body.description),
|
|
684
|
-
schemaElement("body", noRef(body.content[type].schema ?? {}), {
|
|
685
|
-
parseObject: true,
|
|
686
|
-
readOnly: method.method === "GET",
|
|
687
|
-
writeOnly: method.method !== "GET",
|
|
688
|
-
required: body.required ?? false,
|
|
689
|
-
render: ctx,
|
|
690
|
-
allowFile: type === "multipart/form-data"
|
|
691
|
-
})
|
|
692
|
-
);
|
|
693
|
-
}
|
|
694
|
-
const parameterGroups = /* @__PURE__ */ new Map();
|
|
695
|
-
const endpoint = createEndpoint(path, method, ctx.baseUrl);
|
|
696
|
-
for (const param of method.parameters) {
|
|
697
|
-
const schema = noRef(
|
|
698
|
-
param.schema ?? getPreferredMedia(param.content ?? {})?.schema
|
|
699
|
-
);
|
|
700
|
-
if (!schema) continue;
|
|
701
|
-
const content = schemaElement(
|
|
702
|
-
param.name,
|
|
703
|
-
{
|
|
704
|
-
...schema,
|
|
705
|
-
description: param.description ?? schema.description,
|
|
706
|
-
deprecated: (param.deprecated ?? false) || (schema.deprecated ?? false)
|
|
707
|
-
},
|
|
708
|
-
{
|
|
709
|
-
parseObject: false,
|
|
710
|
-
readOnly: method.method === "GET",
|
|
711
|
-
writeOnly: method.method !== "GET",
|
|
712
|
-
required: param.required ?? false,
|
|
713
|
-
render: ctx,
|
|
714
|
-
allowFile: false
|
|
715
|
-
}
|
|
716
|
-
);
|
|
717
|
-
const groupName = {
|
|
718
|
-
path: "Path Parameters",
|
|
719
|
-
query: "Query Parameters",
|
|
720
|
-
header: "Header Parameters",
|
|
721
|
-
cookie: "Cookie Parameters"
|
|
722
|
-
}[param.in] ?? "Other Parameters";
|
|
723
|
-
const group = parameterGroups.get(groupName) ?? [];
|
|
724
|
-
group.push(content);
|
|
725
|
-
parameterGroups.set(groupName, group);
|
|
726
|
-
}
|
|
727
|
-
for (const [group, parameters] of Array.from(parameterGroups.entries())) {
|
|
728
|
-
info.push(heading(level, group), ...parameters);
|
|
729
|
-
}
|
|
730
|
-
info.push(getResponseTable(method));
|
|
731
|
-
const samples = dedupe([
|
|
732
|
-
{
|
|
733
|
-
label: "cURL",
|
|
734
|
-
source: getSampleRequest(endpoint),
|
|
735
|
-
lang: "bash"
|
|
736
|
-
},
|
|
737
|
-
{
|
|
738
|
-
label: "JavaScript",
|
|
739
|
-
source: getSampleRequest2(endpoint),
|
|
740
|
-
lang: "js"
|
|
741
|
-
},
|
|
742
|
-
...ctx.generateCodeSamples ? await ctx.generateCodeSamples(endpoint) : [],
|
|
743
|
-
...method["x-codeSamples"] ?? []
|
|
744
|
-
]);
|
|
745
|
-
example.push(
|
|
746
|
-
ctx.renderer.Requests(
|
|
747
|
-
samples.map((s) => s.label),
|
|
748
|
-
samples.map(
|
|
749
|
-
(s) => ctx.renderer.Request({
|
|
750
|
-
name: s.label,
|
|
751
|
-
code: s.source,
|
|
752
|
-
language: s.lang
|
|
753
|
-
})
|
|
754
|
-
)
|
|
755
|
-
)
|
|
756
|
-
);
|
|
757
|
-
example.push(await getResponseTabs(endpoint, method, ctx));
|
|
758
|
-
return ctx.renderer.API([
|
|
759
|
-
ctx.renderer.APIInfo({ method: method.method, route: path }, info),
|
|
760
|
-
ctx.renderer.APIExample(example)
|
|
761
|
-
]);
|
|
762
|
-
}
|
|
763
|
-
function dedupe(samples) {
|
|
764
|
-
const set = /* @__PURE__ */ new Set();
|
|
765
|
-
const out = [];
|
|
766
|
-
for (let i = samples.length - 1; i >= 0; i--) {
|
|
767
|
-
if (set.has(samples[i].label)) continue;
|
|
768
|
-
set.add(samples[i].label);
|
|
769
|
-
out.unshift(samples[i]);
|
|
770
|
-
}
|
|
771
|
-
return out;
|
|
772
|
-
}
|
|
773
|
-
function getAuthSection(requirements, { document, renderer }) {
|
|
774
|
-
const info = [];
|
|
775
|
-
for (const requirement of requirements) {
|
|
776
|
-
if (info.length > 0) info.push(`---`);
|
|
777
|
-
for (const schema of getScheme(requirement, document)) {
|
|
778
|
-
if (schema.type === "http") {
|
|
779
|
-
info.push(
|
|
780
|
-
renderer.Property(
|
|
781
|
-
{
|
|
782
|
-
name: "Authorization",
|
|
783
|
-
type: {
|
|
784
|
-
basic: "Basic <token>",
|
|
785
|
-
bearer: "Bearer <token>"
|
|
786
|
-
}[schema.scheme] ?? "<token>",
|
|
787
|
-
required: true
|
|
788
|
-
},
|
|
789
|
-
[p(schema.description), `In: \`header\``]
|
|
790
|
-
)
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
if (schema.type === "oauth2") {
|
|
794
|
-
info.push(
|
|
795
|
-
renderer.Property(
|
|
796
|
-
{
|
|
797
|
-
name: "Authorization",
|
|
798
|
-
type: "Bearer <token>",
|
|
799
|
-
required: true
|
|
800
|
-
},
|
|
801
|
-
[
|
|
802
|
-
p(schema.description),
|
|
803
|
-
`In: \`header\``,
|
|
804
|
-
`Scope: \`${schema.scopes.length > 0 ? schema.scopes.join(", ") : "none"}\``
|
|
805
|
-
]
|
|
806
|
-
)
|
|
807
|
-
);
|
|
808
|
-
}
|
|
809
|
-
if (schema.type === "apiKey") {
|
|
810
|
-
info.push(
|
|
811
|
-
renderer.Property(
|
|
812
|
-
{
|
|
813
|
-
name: schema.name,
|
|
814
|
-
type: "<token>",
|
|
815
|
-
required: true
|
|
816
|
-
},
|
|
817
|
-
[p(schema.description), `In: \`${schema.in}\``]
|
|
818
|
-
)
|
|
819
|
-
);
|
|
820
|
-
}
|
|
821
|
-
if (schema.type === "openIdConnect") {
|
|
822
|
-
info.push(
|
|
823
|
-
renderer.Property(
|
|
824
|
-
{
|
|
825
|
-
name: "OpenID Connect",
|
|
826
|
-
type: "<token>",
|
|
827
|
-
required: true
|
|
828
|
-
},
|
|
829
|
-
[p(schema.description)]
|
|
830
|
-
)
|
|
831
|
-
);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
return info.join("\n\n");
|
|
836
|
-
}
|
|
837
|
-
function getResponseTable(operation) {
|
|
838
|
-
const table = [];
|
|
839
|
-
table.push(`| Status code | Description |`);
|
|
840
|
-
table.push(`| ----------- | ----------- |`);
|
|
841
|
-
Object.entries(operation.responses).forEach(([code, value]) => {
|
|
842
|
-
table.push(`| \`${code}\` | ${noRef(value).description} |`);
|
|
843
|
-
});
|
|
844
|
-
return table.join("\n");
|
|
845
|
-
}
|
|
846
|
-
async function getResponseTabs(endpoint, operation, { renderer, generateTypeScriptSchema }) {
|
|
847
|
-
const items = [];
|
|
848
|
-
const child = [];
|
|
849
|
-
for (const code of Object.keys(operation.responses)) {
|
|
850
|
-
const example = getExampleResponse(endpoint, code);
|
|
851
|
-
let ts;
|
|
852
|
-
if (generateTypeScriptSchema) {
|
|
853
|
-
ts = await generateTypeScriptSchema(endpoint, code);
|
|
854
|
-
} else if (generateTypeScriptSchema === void 0) {
|
|
855
|
-
ts = await getTypescriptSchema(endpoint, code);
|
|
856
|
-
}
|
|
857
|
-
const description = code in endpoint.responses ? endpoint.responses[code].schema.description : void 0;
|
|
858
|
-
if (example) {
|
|
859
|
-
items.push(code);
|
|
860
|
-
child.push(
|
|
861
|
-
renderer.Response({ value: code }, [
|
|
862
|
-
p(description),
|
|
863
|
-
renderer.ResponseTypes([
|
|
864
|
-
renderer.ExampleResponse(example),
|
|
865
|
-
...ts ? [renderer.TypeScriptResponse(ts)] : []
|
|
866
|
-
])
|
|
867
|
-
])
|
|
868
|
-
);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
if (items.length === 0) return "";
|
|
872
|
-
return renderer.Responses(
|
|
873
|
-
{
|
|
874
|
-
items
|
|
875
|
-
},
|
|
876
|
-
child
|
|
877
|
-
);
|
|
220
|
+
{startup()}`;
|
|
878
221
|
}
|
|
879
222
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
...document.info,
|
|
896
|
-
context: {
|
|
897
|
-
type: "file",
|
|
898
|
-
routes
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
);
|
|
902
|
-
}
|
|
903
|
-
async function generateOperations(pathOrDocument, options = {}) {
|
|
904
|
-
const document = await Parser.dereference(pathOrDocument);
|
|
905
|
-
const routes = buildRoutes(document).get("all") ?? [];
|
|
906
|
-
const ctx = getContext(document, options);
|
|
907
|
-
return await Promise.all(
|
|
908
|
-
routes.flatMap((route) => {
|
|
909
|
-
return route.methods.map(async (method) => {
|
|
910
|
-
if (!method.operationId)
|
|
911
|
-
throw new Error("Operation ID is required for generating docs.");
|
|
912
|
-
const content = generateDocument(
|
|
913
|
-
ctx.renderer.Root({ baseUrl: ctx.baseUrl }, [
|
|
914
|
-
await renderOperation(route.path, method, ctx, false)
|
|
915
|
-
]),
|
|
916
|
-
options,
|
|
917
|
-
{
|
|
918
|
-
title: method.summary ?? method.method,
|
|
919
|
-
description: method.description,
|
|
920
|
-
context: {
|
|
921
|
-
type: "operation",
|
|
922
|
-
endpoint: method,
|
|
923
|
-
route
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
);
|
|
927
|
-
return {
|
|
928
|
-
id: method.operationId,
|
|
929
|
-
content,
|
|
930
|
-
route
|
|
931
|
-
};
|
|
932
|
-
});
|
|
933
|
-
})
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
async function generateTags(pathOrDocument, options = {}) {
|
|
937
|
-
const document = await Parser.dereference(pathOrDocument);
|
|
938
|
-
const tags = Array.from(buildRoutes(document).entries());
|
|
939
|
-
const ctx = getContext(document, options);
|
|
940
|
-
return await Promise.all(
|
|
941
|
-
tags.filter(([tag]) => tag !== "all").map(async ([tag, routes]) => {
|
|
942
|
-
const info = document.tags?.find((t) => t.name === tag);
|
|
943
|
-
const child = [];
|
|
944
|
-
for (const route of routes) {
|
|
945
|
-
for (const method of route.methods) {
|
|
946
|
-
child.push(await renderOperation(route.path, method, ctx));
|
|
223
|
+
async function generateFiles({ input, output, name: nameFn, per = 'file', cwd = process.cwd(), groupBy = 'none', ...options }) {
|
|
224
|
+
const outputDir = join(cwd, output);
|
|
225
|
+
const resolvedInputs = await fg.glob(input, {
|
|
226
|
+
absolute: true,
|
|
227
|
+
cwd
|
|
228
|
+
});
|
|
229
|
+
await Promise.all(resolvedInputs.map(async (path)=>{
|
|
230
|
+
if (per === 'file') {
|
|
231
|
+
let filename = parse(path).name;
|
|
232
|
+
if (nameFn) filename = nameFn('file', filename);
|
|
233
|
+
const outPath = join(outputDir, `${filename}.mdx`);
|
|
234
|
+
const result = await generateAll(path, options);
|
|
235
|
+
await write(outPath, result);
|
|
236
|
+
console.log(`Generated: ${outPath}`);
|
|
237
|
+
return;
|
|
947
238
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
// src/generate-file.ts
|
|
982
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
983
|
-
import { dirname, join, parse } from "path";
|
|
984
|
-
import fg from "fast-glob";
|
|
985
|
-
async function generateFiles({
|
|
986
|
-
input,
|
|
987
|
-
output,
|
|
988
|
-
name: nameFn,
|
|
989
|
-
per = "file",
|
|
990
|
-
cwd = process.cwd(),
|
|
991
|
-
groupByFolder = false,
|
|
992
|
-
...options
|
|
993
|
-
}) {
|
|
994
|
-
const outputDir = join(cwd, output);
|
|
995
|
-
const resolvedInputs = await fg.glob(input, { absolute: true, cwd });
|
|
996
|
-
await Promise.all(
|
|
997
|
-
resolvedInputs.map(async (path) => {
|
|
998
|
-
let filename = parse(path).name;
|
|
999
|
-
filename = nameFn?.("file", filename) ?? filename;
|
|
1000
|
-
if (per === "file") {
|
|
1001
|
-
const outPath = join(outputDir, `${filename}.mdx`);
|
|
1002
|
-
const result = await generate(path, options);
|
|
1003
|
-
await write(outPath, result);
|
|
1004
|
-
console.log(`Generated: ${outPath}`);
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
if (per === "operation") {
|
|
1008
|
-
const routeFolders = /* @__PURE__ */ new Set();
|
|
1009
|
-
const results2 = await generateOperations(path, options);
|
|
1010
|
-
await Promise.all(
|
|
1011
|
-
results2.map(async (result) => {
|
|
1012
|
-
const outPath = groupByFolder ? join(
|
|
1013
|
-
outputDir,
|
|
1014
|
-
filename,
|
|
1015
|
-
result.route.summary ? getFilename(result.route.summary) : getFilenameFromRoute(result.route.path),
|
|
1016
|
-
`${getFilename(result.id)}.mdx`
|
|
1017
|
-
) : join(outputDir, filename, `${getFilename(result.id)}.mdx`);
|
|
1018
|
-
if (groupByFolder && !routeFolders.has(dirname(outPath))) {
|
|
1019
|
-
routeFolders.add(dirname(outPath));
|
|
1020
|
-
if (result.route.summary) {
|
|
1021
|
-
const metaFile = join(dirname(outPath), "meta.json");
|
|
1022
|
-
await write(
|
|
1023
|
-
metaFile,
|
|
1024
|
-
JSON.stringify({
|
|
1025
|
-
title: result.route.summary
|
|
1026
|
-
})
|
|
1027
|
-
);
|
|
1028
|
-
console.log(`Generated Meta: ${metaFile}`);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
239
|
+
if (per === 'operation') {
|
|
240
|
+
const metaFiles = new Set();
|
|
241
|
+
const results = await generateOperations(path, options);
|
|
242
|
+
await Promise.all(results.map(async (result)=>{
|
|
243
|
+
let outPath;
|
|
244
|
+
if (!result.method.operationId) return;
|
|
245
|
+
const id = result.method.operationId.split('.').at(-1) ?? result.method.operationId;
|
|
246
|
+
if (groupBy === 'tag' && result.method.tags && result.method.tags.length > 0) {
|
|
247
|
+
if (result.method.tags.length > 1) console.warn(`${result.route.path} has more than 1 tag, which isn't allowed under 'groupBy: tag'. Only the first tag will be considered.`);
|
|
248
|
+
outPath = join(outputDir, getFilename(result.method.tags[0]), `${getFilename(id)}.mdx`);
|
|
249
|
+
} else if (groupBy === 'route') {
|
|
250
|
+
outPath = join(outputDir, result.route.summary ? getFilename(result.route.summary) : getFilenameFromRoute(result.route.path), `${getFilename(id)}.mdx`);
|
|
251
|
+
const metaFile = join(dirname(outPath), 'meta.json');
|
|
252
|
+
if (result.route.summary && !metaFiles.has(metaFile)) {
|
|
253
|
+
metaFiles.add(metaFile);
|
|
254
|
+
await write(metaFile, JSON.stringify({
|
|
255
|
+
title: result.route.summary
|
|
256
|
+
}));
|
|
257
|
+
console.log(`Generated Meta: ${metaFile}`);
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
outPath = join(outputDir, `${getFilename(id)}.mdx`);
|
|
261
|
+
}
|
|
262
|
+
await write(outPath, result.content);
|
|
263
|
+
console.log(`Generated: ${outPath}`);
|
|
264
|
+
}));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const results = await generateTags(path, options);
|
|
268
|
+
for (const result of results){
|
|
269
|
+
let tagName = result.tag;
|
|
270
|
+
tagName = nameFn?.('tag', tagName) ?? getFilename(tagName);
|
|
271
|
+
const outPath = join(outputDir, `${tagName}.mdx`);
|
|
1031
272
|
await write(outPath, result.content);
|
|
1032
273
|
console.log(`Generated: ${outPath}`);
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1037
|
-
const results = await generateTags(path, options);
|
|
1038
|
-
for (const result of results) {
|
|
1039
|
-
let tagName = result.tag;
|
|
1040
|
-
tagName = nameFn?.("tag", tagName) ?? getFilename(tagName);
|
|
1041
|
-
const outPath = join(outputDir, filename, `${tagName}.mdx`);
|
|
1042
|
-
await write(outPath, result.content);
|
|
1043
|
-
console.log(`Generated: ${outPath}`);
|
|
1044
|
-
}
|
|
1045
|
-
})
|
|
1046
|
-
);
|
|
274
|
+
}
|
|
275
|
+
}));
|
|
1047
276
|
}
|
|
1048
277
|
function getFilenameFromRoute(path) {
|
|
1049
|
-
|
|
278
|
+
return path.replaceAll('.', '/').split('/').filter((v)=>!v.startsWith('{') && !v.endsWith('}')).at(-1) ?? '';
|
|
1050
279
|
}
|
|
1051
280
|
function getFilename(s) {
|
|
1052
|
-
|
|
1053
|
-
/[A-Z]/g,
|
|
1054
|
-
(match, idx) => idx === 0 ? match : `-${match.toLowerCase()}`
|
|
1055
|
-
).replace(/\s+/g, "-").toLowerCase();
|
|
281
|
+
return s.replace(/[A-Z]/g, (match, idx)=>idx === 0 ? match : `-${match.toLowerCase()}`).replace(/\s+/g, '-').toLowerCase();
|
|
1056
282
|
}
|
|
1057
283
|
async function write(path, content) {
|
|
1058
|
-
|
|
1059
|
-
|
|
284
|
+
await mkdir(dirname(path), {
|
|
285
|
+
recursive: true
|
|
286
|
+
});
|
|
287
|
+
await writeFile(path, content);
|
|
1060
288
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
defaultRenderer,
|
|
1064
|
-
generate,
|
|
1065
|
-
generateFiles,
|
|
1066
|
-
generateOperations,
|
|
1067
|
-
generateTags
|
|
1068
|
-
};
|
|
289
|
+
|
|
290
|
+
export { generateAll, generateFiles, generateOperations, generateTags };
|