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.js CHANGED
@@ -1,1068 +1,290 @@
1
- // src/generate.ts
2
- import Parser from "@apidevtools/json-schema-ref-parser";
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
- // src/build-routes.ts
5
- var methodKeys = [
6
- "get",
7
- "post",
8
- "patch",
9
- "delete",
10
- "head",
11
- "put"
12
- ];
13
- function buildRoutes(document) {
14
- const map = /* @__PURE__ */ new Map();
15
- for (const [path, value] of Object.entries(document.paths)) {
16
- if (!value) continue;
17
- const methodMap = /* @__PURE__ */ new Map();
18
- for (const methodKey of methodKeys) {
19
- const operation = value[methodKey];
20
- if (!operation) continue;
21
- const info = buildOperation(methodKey, operation);
22
- const tags = operation.tags ?? [];
23
- for (const tag of [...tags, "all"]) {
24
- const list = methodMap.get(tag) ?? [];
25
- list.push(info);
26
- methodMap.set(tag, list);
27
- }
28
- }
29
- for (const [tag, methods] of methodMap.entries()) {
30
- const list = map.get(tag) ?? [];
31
- list.push({
32
- ...value,
33
- path,
34
- methods
35
- });
36
- map.set(tag, list);
37
- }
38
- }
39
- return map;
40
- }
41
- function buildOperation(method, operation) {
42
- return {
43
- ...operation,
44
- parameters: operation.parameters ?? [],
45
- method: method.toUpperCase()
46
- };
8
+ function createMethod(method, operation) {
9
+ return {
10
+ ...operation,
11
+ parameters: operation.parameters ?? [],
12
+ method: method.toUpperCase()
13
+ };
47
14
  }
48
15
 
49
- // src/utils/generate-document.ts
50
- import { dump } from "js-yaml";
51
-
52
- // src/render/element.ts
53
- function createElement(name, props, ...child) {
54
- const s = [];
55
- const params = Object.entries(props).map(([key, value]) => `${key}={${JSON.stringify(value)}}`).join(" ");
56
- s.push(params.length > 0 ? `<${name} ${params}>` : `<${name}>`);
57
- s.push(...child.filter((v) => v.length > 0));
58
- s.push(`</${name}>`);
59
- return s.join("\n\n");
60
- }
61
- function p(child) {
62
- if (!child) return "";
63
- return child.replace("<", "\\<").replace(">", "\\>");
64
- }
65
- function span(child) {
66
- return `<span>${p(child)}</span>`;
67
- }
68
- function codeblock({ language, title }, child) {
69
- return [
70
- title ? `\`\`\`${language} title=${JSON.stringify(title)}` : `\`\`\`${language}`,
71
- child.trim(),
72
- "```"
73
- ].join("\n");
74
- }
75
- function heading(depth, child) {
76
- return `${"#".repeat(depth)} ${child.trim()}`;
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
- const banner = dump({
104
- title: frontmatter.title,
105
- description: frontmatter.description,
106
- full: true,
107
- ...frontmatter.context.type === "operation" ? {
108
- method: frontmatter.context.endpoint.method,
109
- route: frontmatter.context.route.path
110
- } : void 0,
111
- ...options.frontmatter?.(
112
- frontmatter.title,
113
- frontmatter.description,
114
- frontmatter.context
115
- )
116
- }).trim();
117
- const finalImports = (options.imports ?? [
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
- ]).map(
123
- (item) => `import { ${item.names.join(", ")} } from ${JSON.stringify(item.from)};`
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
- const result = [];
137
- for (const c of id) {
138
- if (result.length === 0) result.push(c.toLocaleUpperCase());
139
- else if (/^[A-Z]$/.test(c) && result.at(-1) !== " ") result.push(" ", c);
140
- else if (c === "-") result.push(" ");
141
- else result.push(c);
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
- // src/requests/curl.ts
257
- function getSampleRequest(endpoint) {
258
- const s = [];
259
- s.push(`curl -X ${endpoint.method} "${endpoint.url}"`);
260
- for (const param of endpoint.parameters) {
261
- if (param.in === "header") {
262
- const header = `${param.name}: ${toSampleInput(param.sample)}`;
263
- s.push(`-H "${header}"`);
264
- }
265
- }
266
- if (endpoint.body?.mediaType === "multipart/form-data")
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
- options.set("method", JSON.stringify(endpoint.method));
283
- if (Object.keys(headers).length > 0) {
284
- options.set("headers", JSON.stringify(headers, void 0, 2));
285
- }
286
- if (endpoint.body?.mediaType === "multipart/form-data" && typeof endpoint.body.sample === "object" && endpoint.body.sample) {
287
- s.push(`const formData = new FormData();`);
288
- for (const [key, value] of Object.entries(endpoint.body.sample))
289
- s.push(`formData.set(${key}, ${JSON.stringify(value)})`);
290
- options.set("body", "formData");
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
- // src/utils/get-security.ts
318
- function getScheme(requirement, document) {
319
- const results = [];
320
- const schemas = document.components?.securitySchemes ?? {};
321
- for (const [key, scopes] of Object.entries(requirement)) {
322
- if (!(key in schemas)) return [];
323
- const schema = noRef(schemas[key]);
324
- results.push({
325
- ...schema,
326
- scopes
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 parameterToField(v, ctx) {
386
- return {
387
- name: v.name,
388
- ...toSchema(
389
- noRef(v.schema) ?? { type: "string" },
390
- v.required ?? false,
391
- ctx
392
- )
393
- };
394
- }
395
- function toReference(schema, required, ctx) {
396
- return {
397
- type: "ref",
398
- isRequired: required,
399
- schema: getIdFromSchema(schema, false, ctx)
400
- };
401
- }
402
- function toSchema(schema, required, ctx) {
403
- if (schema.type === "array") {
404
- return {
405
- type: "array",
406
- description: schema.description ?? schema.title,
407
- isRequired: required,
408
- items: getIdFromSchema(noRef(schema.items), false, ctx)
409
- };
410
- }
411
- if (schema.type === "object" || schema.properties !== void 0 || schema.allOf !== void 0) {
412
- const properties = {};
413
- Object.entries(schema.properties ?? {}).forEach(([key, prop]) => {
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
- const additional = noRef(schema.additionalProperties);
425
- let additionalProperties;
426
- if (additional && typeof additional === "object") {
427
- if (!additional.type && !additional.anyOf && !additional.allOf && !additional.oneOf) {
428
- additionalProperties = true;
429
- } else {
430
- additionalProperties = getIdFromSchema(additional, false, ctx);
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
- if (schema.type === void 0) {
444
- const combine = schema.anyOf ?? schema.oneOf;
445
- if (combine) {
446
- return {
447
- type: "switcher",
448
- description: schema.description ?? schema.title,
449
- items: Object.fromEntries(
450
- combine.map((c, idx) => {
451
- const item = noRef(c);
452
- return [
453
- item.title ?? item.type ?? `Item ${idx.toString()}`,
454
- toReference(item, true, ctx)
455
- ];
456
- })
457
- ),
458
- isRequired: required
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
- // src/render/schema.ts
482
- var keys = {
483
- example: "Example",
484
- default: "Default",
485
- minimum: "Minimum",
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
- Object.entries(properties ?? {}).forEach(([key, value]) => {
531
- child.push(
532
- render(key, noRef(value), {
533
- ...ctx,
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
- // src/render/operation.ts
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
- // src/generate.ts
881
- async function generate(pathOrDocument, options = {}) {
882
- const document = await Parser.dereference(pathOrDocument);
883
- const routes = buildRoutes(document).get("all") ?? [];
884
- const ctx = getContext(document, options);
885
- const child = [];
886
- for (const route of routes) {
887
- for (const method of route.methods) {
888
- child.push(await renderOperation(route.path, method, ctx));
889
- }
890
- }
891
- return generateDocument(
892
- ctx.renderer.Root({ baseUrl: ctx.baseUrl }, child),
893
- options,
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
- return {
950
- tag,
951
- content: generateDocument(
952
- ctx.renderer.Root({ baseUrl: ctx.baseUrl }, child),
953
- options,
954
- {
955
- title: idToTitle(tag),
956
- description: info?.description,
957
- context: {
958
- type: "tag",
959
- tag: info,
960
- routes
961
- }
962
- }
963
- )
964
- };
965
- })
966
- );
967
- }
968
- function getContext(document, options) {
969
- return {
970
- document,
971
- renderer: {
972
- ...defaultRenderer,
973
- ...options.renderer
974
- },
975
- generateTypeScriptSchema: options.generateTypeScriptSchema,
976
- generateCodeSamples: options.generateCodeSamples,
977
- baseUrl: document.servers?.[0].url ?? "https://example.com"
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
- return path.split("/").filter((v) => !v.startsWith("{") && !v.endsWith("}")).at(-1) ?? "";
278
+ return path.replaceAll('.', '/').split('/').filter((v)=>!v.startsWith('{') && !v.endsWith('}')).at(-1) ?? '';
1050
279
  }
1051
280
  function getFilename(s) {
1052
- return s.replace(
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
- await mkdir(dirname(path), { recursive: true });
1059
- await writeFile(path, content);
284
+ await mkdir(dirname(path), {
285
+ recursive: true
286
+ });
287
+ await writeFile(path, content);
1060
288
  }
1061
- export {
1062
- createElement,
1063
- defaultRenderer,
1064
- generate,
1065
- generateFiles,
1066
- generateOperations,
1067
- generateTags
1068
- };
289
+
290
+ export { generateAll, generateFiles, generateOperations, generateTags };