UNPKG

fumadocs-openapi

Version:

Generate MDX docs for your OpenAPI spec

411 lines (409 loc) 14.9 kB
import { idToTitle } from "../../utils/id-to-title.js"; import { createMethod, methodKeys } from "../../utils/schema.js"; import { isMediaTypeSupported } from "../../requests/media/resolve-adapter.js"; import "../../requests/media/adapter.js"; import { cn } from "../../utils/cn.js"; import { MethodLabel } from "../components/method-label.js"; import { APIPlayground } from "../../playground/index.js"; import { Schema } from "../schema/index.js"; import { UsageTabsProviderLazy } from "./usage-tabs/lazy.js"; import { AccordionContent, AccordionHeader, AccordionItem, AccordionTrigger, Accordions } from "../components/accordion.js"; import { UsageTabs } from "./usage-tabs/index.js"; import { getTypescriptSchema } from "../../utils/get-typescript-schema.js"; import { CopyResponseTypeScript, SelectTab, SelectTabTrigger, SelectTabs } from "./client.js"; import { RequestTabs, getExampleRequests } from "./request-tabs.js"; import { Fragment } from "react"; import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime"; //#region src/ui/operation/index.tsx const ParamTypes = { path: "Path Parameters", query: "Query Parameters", header: "Header Parameters", cookie: "Cookie Parameters" }; async function Operation({ type = "operation", path, method, ctx, showTitle, showDescription, headingLevel = 2 }) { const { schema: { dereferenced } } = ctx; const body = method.requestBody; let headNode = null; const descriptionNode = showDescription && method.description && ctx.renderMarkdown(method.description); let bodyNode = null; let authNode = null; let responseNode = null; let callbacksNode = null; if (showTitle) { const title = method.summary || (method.operationId ? idToTitle(method.operationId) : path); headNode = ctx.renderHeading(headingLevel, title); headingLevel++; } const contentTypes = body ? Object.entries(body.content) : null; if (body && contentTypes && contentTypes.length > 0) { const items = contentTypes.map(([key]) => ({ label: /* @__PURE__ */ jsx("code", { className: "text-xs", children: key }), value: key })); bodyNode = /* @__PURE__ */ jsxs(SelectTabs, { defaultValue: items[0].value, children: [ /* @__PURE__ */ jsxs("div", { className: "flex gap-2 items-center justify-between mt-10", children: [ctx.renderHeading(headingLevel, "Request Body", { className: "my-0!" }), contentTypes.length > 1 ? /* @__PURE__ */ jsx(SelectTabTrigger, { items, className: "font-medium" }) : /* @__PURE__ */ jsx("p", { className: "text-fd-muted-foreground not-prose", children: items[0].label })] }), body.description && ctx.renderMarkdown(body.description), contentTypes.map(([type$1, content]) => { if (!isMediaTypeSupported(type$1, ctx.mediaAdapters)) throw new Error(`Media type ${type$1} is not supported (in ${path})`); return /* @__PURE__ */ jsx(SelectTab, { value: type$1, children: /* @__PURE__ */ jsx(Schema, { client: { name: "body", as: "body", required: body.required }, root: content.schema ?? {}, readOnly: method.method === "GET", writeOnly: method.method !== "GET", ctx }) }, type$1); }) ] }); } if (method.responses && ctx.showResponseSchema !== false) { const statuses = Object.keys(method.responses); responseNode = /* @__PURE__ */ jsxs(Fragment$1, { children: [ctx.renderHeading(headingLevel, "Response Body"), /* @__PURE__ */ jsx(Accordions, { type: "multiple", children: statuses.map((status) => /* @__PURE__ */ jsx(ResponseAccordion, { status, operation: method, ctx }, status)) })] }); } const parameterNode = Object.entries(ParamTypes).map(([type$1, title]) => { const params = method.parameters?.filter((param) => param.in === type$1); if (!params || params.length === 0) return; return /* @__PURE__ */ jsxs(Fragment, { children: [ctx.renderHeading(headingLevel, title), /* @__PURE__ */ jsx("div", { className: "flex flex-col", children: params.map((param) => /* @__PURE__ */ jsx(Schema, { client: { name: param.name, required: param.required }, root: { ...param.schema, description: param.description ?? param.schema?.description, deprecated: (param.deprecated ?? false) || (param.schema?.deprecated ?? false) }, readOnly: method.method === "GET", writeOnly: method.method !== "GET", ctx }, param.name)) })] }, type$1); }); const securities = (method.security ?? dereferenced.security ?? []).filter((v) => Object.keys(v).length > 0); if (type === "operation" && securities.length > 0) { const securitySchemes = dereferenced.components?.securitySchemes; const items = securities.map((security, i) => { return { value: String(i), label: /* @__PURE__ */ jsx("div", { className: "flex flex-col text-xs min-w-0", children: Object.entries(security).map(([key, scopes]) => /* @__PURE__ */ jsxs("code", { className: "truncate", children: [ /* @__PURE__ */ jsx("span", { className: "font-medium", children: key }), " ", scopes.length > 0 && /* @__PURE__ */ jsx("span", { className: "text-fd-muted-foreground", children: scopes.join(", ") }) ] }, key)) }) }; }); authNode = /* @__PURE__ */ jsxs(SelectTabs, { defaultValue: items[0].value, children: [/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2 mt-10", children: [ctx.renderHeading(headingLevel, "Authorization", { className: "my-0!" }), items.length > 1 ? /* @__PURE__ */ jsx(SelectTabTrigger, { items }) : /* @__PURE__ */ jsx("div", { className: "not-prose", children: items[0].label })] }), securities.map((security, i) => /* @__PURE__ */ jsx(SelectTab, { value: items[i].value, children: Object.entries(security).map(([key, scopes]) => { const scheme = securitySchemes?.[key]; if (!scheme) return; return /* @__PURE__ */ jsx(AuthScheme, { scheme, scopes, ctx }, key); }) }, i))] }); } const callbacks = method.callbacks ? Object.entries(method.callbacks) : null; if (callbacks && callbacks.length > 0) { const items = callbacks.map(([key]) => ({ label: /* @__PURE__ */ jsx("code", { className: "text-xs", children: key }), value: key })); callbacksNode = /* @__PURE__ */ jsxs(SelectTabs, { defaultValue: items[0].value, children: [/* @__PURE__ */ jsxs("div", { className: "flex justify-between gap-2 items-end mt-10", children: [ctx.renderHeading(headingLevel, "Callbacks", { className: "my-0!" }), callbacks.length > 1 ? /* @__PURE__ */ jsx(SelectTabTrigger, { items, className: "font-medium" }) : /* @__PURE__ */ jsx("p", { className: "text-fd-muted-foreground not-prose", children: items[0].label })] }), callbacks.map(([name, callback]) => /* @__PURE__ */ jsx(SelectTab, { value: name, children: /* @__PURE__ */ jsx(WebhookCallback, { callback, ctx, headingLevel }) }, name))] }); } let { renderOperationLayout, renderWebhookLayout } = ctx.content ?? {}; if (type === "operation") { renderOperationLayout ??= (slots) => { return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-x-6 gap-y-4 @4xl:flex-row @4xl:items-start", children: [/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [ slots.header, slots.apiPlayground, slots.description, slots.authSchemes, slots.paremeters, slots.body, slots.responses, slots.callbacks ] }), /* @__PURE__ */ jsx("div", { className: "@4xl:sticky @4xl:top-[calc(var(--fd-docs-row-1,2rem)+1rem)] @4xl:w-[400px]", children: slots.apiExample })] }); }; const playgroundEnabled = ctx.playground?.enabled ?? true; const content = await renderOperationLayout({ header: headNode, description: descriptionNode, authSchemes: authNode, body: bodyNode, callbacks: callbacksNode, paremeters: parameterNode, responses: responseNode, apiPlayground: playgroundEnabled ? /* @__PURE__ */ jsx(APIPlayground, { path, method, ctx }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-row items-center gap-2.5 p-3 rounded-xl border bg-fd-card text-fd-card-foreground not-prose", children: [/* @__PURE__ */ jsx(MethodLabel, { className: "text-xs", children: method.method }), /* @__PURE__ */ jsx("code", { className: "flex-1 overflow-auto text-nowrap text-[0.8125rem] text-fd-muted-foreground", children: path })] }), apiExample: /* @__PURE__ */ jsx(UsageTabs, { method, ctx }) }, ctx, method); return /* @__PURE__ */ jsx(UsageTabsProviderLazy, { defaultExampleId: method["x-exclusiveCodeSample"] ?? method["x-selectedCodeSample"], route: path, examples: getExampleRequests(path, method, ctx), children: content }); } else { renderWebhookLayout ??= (slots) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col-reverse gap-x-6 gap-y-4 @4xl:flex-row @4xl:items-start", children: [/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [ slots.header, slots.description, slots.authSchemes, slots.paremeters, slots.body, slots.responses, slots.callbacks ] }), /* @__PURE__ */ jsx("div", { className: "@4xl:sticky @4xl:top-[calc(var(--fd-docs-row-1,2rem)+1rem)] @4xl:w-[400px]", children: slots.requests })] }); return renderWebhookLayout({ header: headNode, description: descriptionNode, authSchemes: authNode, body: bodyNode, callbacks: callbacksNode, paremeters: parameterNode, responses: responseNode, requests: /* @__PURE__ */ jsx(RequestTabs, { path, operation: method, ctx }) }); } } async function ResponseAccordion({ status, operation, ctx }) { const response = operation.responses[status]; const { generateTypeScriptSchema } = ctx; const contentTypes = response.content ? Object.entries(response.content) : []; let wrapper = (children) => children; let selectorNode = null; if (contentTypes.length > 0) { const items = contentTypes.map(([key]) => ({ label: /* @__PURE__ */ jsx("code", { className: "text-xs", children: key }), value: key })); selectorNode = items.length === 1 ? /* @__PURE__ */ jsx("p", { className: "text-fd-muted-foreground not-prose", children: items[0].label }) : /* @__PURE__ */ jsx(SelectTabTrigger, { items }); wrapper = (children) => /* @__PURE__ */ jsx(SelectTabs, { defaultValue: items[0].value, children }); } return wrapper(/* @__PURE__ */ jsxs(AccordionItem, { value: status, children: [/* @__PURE__ */ jsxs(AccordionHeader, { children: [/* @__PURE__ */ jsx(AccordionTrigger, { className: "font-mono", children: status }), selectorNode] }), /* @__PURE__ */ jsxs(AccordionContent, { className: "ps-4.5", children: [response.description && /* @__PURE__ */ jsx("div", { className: "prose-no-margin mb-2", children: ctx.renderMarkdown(response.description) }), contentTypes.map(async ([type, resType]) => { const schema = resType.schema; let ts; if (generateTypeScriptSchema) ts = await generateTypeScriptSchema(operation, status); else if (generateTypeScriptSchema === void 0 && schema) ts = await getTypescriptSchema(schema, ctx); return /* @__PURE__ */ jsxs(SelectTab, { value: type, className: "mb-2", children: [ts && /* @__PURE__ */ jsx(CopyResponseTypeScript, { code: ts }), schema && /* @__PURE__ */ jsx("div", { className: "border px-3 py-2 rounded-lg", children: /* @__PURE__ */ jsx(Schema, { client: { name: "response", as: "body" }, root: schema, readOnly: true, ctx }) })] }, type); })] })] })); } function WebhookCallback({ callback, ctx, headingLevel }) { return /* @__PURE__ */ jsx(Accordions, { type: "single", collapsible: true, children: Object.entries(callback).map(([path, pathItem]) => { const pathNodes = methodKeys.map((method) => { const operation = pathItem[method]; if (!operation) return null; return /* @__PURE__ */ jsx("div", { className: "border p-3 my-2 @container prose-no-margin rounded-lg", children: /* @__PURE__ */ jsx(Operation, { type: "webhook", path, headingLevel: headingLevel + 1, method: createMethod(method, pathItem, operation), ctx }) }, method); }); return /* @__PURE__ */ jsxs(AccordionItem, { value: path, children: [/* @__PURE__ */ jsx(AccordionHeader, { children: /* @__PURE__ */ jsx(AccordionTrigger, { className: "font-mono", children: path }) }), /* @__PURE__ */ jsx(AccordionContent, { children: pathNodes })] }, path); }) }); } function AuthScheme({ scheme: schema, scopes, ctx }) { if (schema.type === "http" || schema.type === "oauth2") return /* @__PURE__ */ jsxs(AuthProperty, { name: "Authorization", type: schema.type === "http" && schema.scheme === "basic" ? `Basic <token>` : "Bearer <token>", scopes, children: [schema.description && ctx.renderMarkdown(schema.description), /* @__PURE__ */ jsxs("p", { children: ["In: ", /* @__PURE__ */ jsx("code", { children: "header" })] })] }); if (schema.type === "apiKey") return /* @__PURE__ */ jsxs(AuthProperty, { name: schema.name, type: "<token>", scopes, children: [schema.description && ctx.renderMarkdown(schema.description), /* @__PURE__ */ jsxs("p", { children: ["In: ", /* @__PURE__ */ jsx("code", { children: schema.in })] })] }); if (schema.type === "openIdConnect") return /* @__PURE__ */ jsx(AuthProperty, { name: "OpenID Connect", type: "<token>", scopes, children: schema.description && ctx.renderMarkdown(schema.description) }); } function AuthProperty({ name, type, scopes = [], className, ...props }) { return /* @__PURE__ */ jsxs("div", { className: cn("text-sm border-t my-4 first:border-t-0", className), children: [/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3 not-prose", children: [/* @__PURE__ */ jsx("span", { className: "font-medium font-mono text-fd-primary", children: name }), /* @__PURE__ */ jsx("span", { className: "text-sm font-mono text-fd-muted-foreground", children: type })] }), /* @__PURE__ */ jsxs("div", { className: "prose-no-margin pt-2.5 empty:hidden", children: [props.children, scopes.length > 0 && /* @__PURE__ */ jsxs("p", { children: ["Scope: ", /* @__PURE__ */ jsx("code", { children: scopes.join(", ") })] })] })] }); } //#endregion export { Operation }; //# sourceMappingURL=index.js.map