UNPKG

fumadocs-openapi

Version:

Generate MDX docs for your OpenAPI spec

197 lines (196 loc) 7.02 kB
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)}} />`; }