fumadocs-openapi
Version:
Generate MDX docs for your OpenAPI spec
197 lines (196 loc) • 7.02 kB
JavaScript
import { getAPIPageItems } from './build-routes.js';
import { idToTitle } from './utils/id-to-title.js';
import { dump } from 'js-yaml';
import Slugger from 'github-slugger';
import { removeUndefined } from './utils/remove-undefined.js';
export function generateAll(schemaId, processed, options = {}) {
const { dereferenced } = processed;
const items = getAPIPageItems(dereferenced);
return generatePage(schemaId, processed, {
operations: items.operations,
webhooks: items.webhooks,
hasHead: true,
}, {
...options,
title: dereferenced.info.title,
description: dereferenced.info.description,
}, {
type: 'file',
});
}
export function generatePages(schemaId, processed, options = {}) {
const { dereferenced } = processed;
const items = getAPIPageItems(dereferenced);
const result = [];
for (const item of items.operations) {
const pathItem = dereferenced.paths?.[item.path];
if (!pathItem)
continue;
const operation = pathItem[item.method];
if (!operation)
continue;
result.push({
type: 'operation',
item,
content: generatePage(schemaId, processed, {
operations: [item],
hasHead: false,
}, {
...options,
title: operation.summary ??
pathItem.summary ??
idToTitle(operation.operationId ?? 'unknown'),
description: operation.description ?? pathItem.description,
}, {
type: 'operation',
}),
});
}
for (const item of items.webhooks) {
const pathItem = dereferenced.webhooks?.[item.name];
if (!pathItem)
continue;
const operation = pathItem[item.method];
if (!operation)
continue;
result.push({
type: 'webhook',
item,
content: generatePage(schemaId, processed, {
webhooks: [item],
hasHead: false,
}, {
...options,
title: operation.summary ?? pathItem.summary ?? idToTitle(item.name),
description: operation.description ?? pathItem.description,
}, {
type: 'operation',
}),
});
}
return result;
}
export function generateTags(schemaId, processed, options = {}) {
const { dereferenced } = processed;
if (!dereferenced.tags)
return [];
const items = getAPIPageItems(dereferenced);
return dereferenced.tags.map((tag) => {
const webhooks = items.webhooks.filter((v) => v.tags && v.tags.includes(tag.name));
const operations = items.operations.filter((v) => v.tags && v.tags.includes(tag.name));
const displayName = tag && 'x-displayName' in tag && typeof tag['x-displayName'] === 'string'
? tag['x-displayName']
: idToTitle(tag.name);
return {
tag: tag.name,
content: generatePage(schemaId, processed, {
operations,
webhooks,
hasHead: true,
}, {
...options,
title: displayName,
description: tag?.description,
}, {
type: 'tag',
tag,
}),
};
});
}
export function generateDocument(frontmatter, content, options) {
const { addGeneratedComment = true, imports } = options;
const out = [];
const banner = dump(removeUndefined(frontmatter)).trimEnd();
if (banner.length > 0)
out.push(`---\n${banner}\n---`);
if (addGeneratedComment) {
let commentContent = 'This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again.';
if (typeof addGeneratedComment === 'string') {
commentContent = addGeneratedComment;
}
commentContent = commentContent.replaceAll('/', '\\/');
out.push(`{/* ${commentContent} */}`);
}
if (imports) {
out.push(...imports
.map((item) => `import { ${item.names.join(', ')} } from ${JSON.stringify(item.from)};`)
.join('\n'));
}
out.push(content);
return out.join('\n\n');
}
function generatePage(schemaId, processed, pageProps, options, context) {
const { frontmatter, includeDescription = false } = options;
const extend = frontmatter?.(options.title, options.description, context);
const page = {
...pageProps,
document: schemaId,
};
let meta;
if (page.operations?.length === 1) {
const operation = page.operations[0];
meta = {
method: operation.method.toUpperCase(),
route: operation.path,
};
}
const data = generateStaticData(processed.dereferenced, page);
const content = [];
if (options.description && includeDescription)
content.push(options.description);
content.push(pageContent(page));
return generateDocument({
title: options.title,
description: !includeDescription ? options.description : undefined,
full: true,
...extend,
_openapi: {
...meta,
...data,
...extend?._openapi,
},
}, content.join('\n\n'), options);
}
function generateStaticData(dereferenced, props) {
const slugger = new Slugger();
const toc = [];
const structuredData = { headings: [], contents: [] };
for (const item of props.operations ?? []) {
const operation = dereferenced.paths?.[item.path]?.[item.method];
if (!operation)
continue;
if (props.hasHead && operation.operationId) {
const title = operation.summary ??
(operation.operationId ? idToTitle(operation.operationId) : item.path);
const id = slugger.slug(title);
toc.push({
depth: 2,
title,
url: `#${id}`,
});
structuredData.headings.push({
content: title,
id,
});
}
if (operation.description)
structuredData.contents.push({
content: operation.description,
heading: structuredData.headings.at(-1)?.id,
});
}
return { toc, structuredData };
}
function pageContent(props) {
// filter extra properties in props
const operations = (props.operations ?? []).map((item) => ({
path: item.path,
method: item.method,
}));
const webhooks = (props.webhooks ?? []).map((item) => ({
name: item.name,
method: item.method,
}));
return `<APIPage document={${JSON.stringify(props.document)}} operations={${JSON.stringify(operations)}} webhooks={${JSON.stringify(webhooks)}} hasHead={${JSON.stringify(props.hasHead)}} />`;
}