OpenAPI: Support accessing context information on custom frontmatter

This commit is contained in:
fuma-nama
2024-08-02 13:04:41 +08:00
parent 1319650ffd
commit 744bd248b8
12 changed files with 147 additions and 30 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Fix generate files on `operation` mode
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': minor
---
Support accessing context information on custom frontmatter
+2 -3
View File
@@ -10,9 +10,8 @@ apps/docs/shaders/*
**/test/out/**/*
**/test/fixtures/*.output.*
apps/docs/content/docs/ui/museum/events.mdx
apps/docs/content/docs/ui/museum/operations.mdx
apps/docs/content/docs/ui/museum/tickets.mdx
apps/docs/content/docs/ui/museum/*.mdx
apps/docs/content/docs/ui/museum/!index.mdx
apps/docs/content/docs/ui/typescript.mdx
+2 -3
View File
@@ -1,5 +1,4 @@
out
content/docs/ui/museum/events.mdx
content/docs/ui/museum/operations.mdx
content/docs/ui/museum/tickets.mdx
content/docs/ui/museum/*.mdx
!content/docs/ui/museum/index.mdx
content/docs/ui/(integrations)/typescript.mdx
+42 -3
View File
@@ -1,9 +1,48 @@
import { type ReactNode } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/utils/cn';
const badgeVariants = cva(
'rounded-full border px-2 py-0.5 text-xs font-medium',
{
variants: {
color: {
green:
'border-green-400/50 bg-green-400/20 text-green-600 dark:text-green-400',
yellow:
'border-yellow-400/50 bg-yellow-400/20 text-yellow-600 dark:text-yellow-400',
red: 'border-red-400/50 bg-red-400/20 text-red-600 dark:text-red-400',
blue: 'border-blue-400/50 bg-blue-400/20 text-blue-600 dark:text-blue-400',
orange:
'border-orange-400/50 bg-orange-400/20 text-orange-600 dark:text-orange-400',
},
},
},
);
export function getBadgeColor(
method: string,
): VariantProps<typeof badgeVariants>['color'] {
switch (method) {
case 'PUT':
return 'yellow';
case 'PATCH':
return 'orange';
case 'POST':
return 'blue';
case 'DELETE':
return 'red';
default:
return 'green';
}
}
export function ApiIndicator({ method }: { method: string }): ReactNode {
const color = getBadgeColor(method);
export function ApiIndicator(): ReactNode {
return (
<span className="ms-auto rounded-full bg-fd-primary px-2 py-0.5 text-xs font-medium text-fd-primary-foreground">
API
<span className={cn(badgeVariants({ className: 'ms-auto', color }))}>
{method}
</span>
);
}
+1 -1
View File
@@ -5,7 +5,7 @@ import * as path from 'node:path';
void OpenAPI.generateFiles({
input: ['./*.yaml'],
output: './content/docs/ui',
per: 'tag',
per: 'operation',
renderer: {
Root(props, child) {
return OpenAPI.createElement(
+1 -2
View File
@@ -6,10 +6,9 @@ import type { SearchIndex } from 'fumadocs-mdx';
env.loadEnvConfig(process.cwd());
const path = '.next/server/chunks/fumadocs_search.json';
async function main() {
const indexes = JSON.parse(
(await readFile(path)).toString(),
(await readFile('.next/server/chunks/fumadocs_search.json')).toString(),
) as SearchIndex[];
await Promise.all([writeOgImages(indexes), updateSearchIndexes(indexes)]);
+9 -1
View File
@@ -10,6 +10,10 @@ import { ApiIndicator } from '@/components/ui/api-indicator';
const frontmatter = defaultSchemas.frontmatter.extend({
preview: z.string().optional(),
index: z.boolean().default(false),
/**
* API routes only
*/
method: z.string().optional(),
});
export const utils = loader({
@@ -30,7 +34,11 @@ export const utils = loader({
const data = file.data.data as z.output<typeof frontmatter>;
if (!data.full) return node;
node.name = [node.name, ' ', ApiIndicator()];
node.name = [
node.name,
' ',
ApiIndicator({ method: data.method ?? 'API' }),
];
return node;
},
},
+1
View File
@@ -71,6 +71,7 @@ export async function generateFiles({
console.log(`Generated: ${outPath}`);
}),
);
return;
}
const results = await generateTags(path, options);
+56 -8
View File
@@ -3,10 +3,38 @@ import type { OpenAPIV3 as OpenAPI } from 'openapi-types';
import { buildRoutes } from '@/build-routes';
import { generateDocument } from '@/utils/generate-document';
import { idToTitle } from '@/utils/id-to-title';
import type { RenderContext } from './types';
import type {
MethodInformation,
RenderContext,
RouteInformation,
} from './types';
import { defaultRenderer, type Renderer } from './render/renderer';
import { renderOperation } from './render/operation';
export type DocumentContext =
| {
type: 'tag';
tag: OpenAPI.TagObject | undefined;
routes: RouteInformation[];
}
| {
type: 'operation';
/**
* information of the route
*/
route: RouteInformation;
/**
* information of the method (API Endpoint)
*/
endpoint: MethodInformation;
}
| {
type: 'file';
routes: RouteInformation[];
};
export interface GenerateOptions
extends Pick<
RenderContext,
@@ -30,6 +58,7 @@ export interface GenerateOptions
frontmatter?: (
title: string,
description: string | undefined,
context: DocumentContext,
) => Record<string, unknown>;
renderer?: Partial<Renderer>;
@@ -61,10 +90,15 @@ export async function generate(
}
return generateDocument(
document.info.title,
document.info.description,
ctx.renderer.Root({ baseUrl: ctx.baseUrl }, child),
options,
{
...document.info,
context: {
type: 'file',
routes,
},
},
);
}
@@ -80,12 +114,19 @@ export async function generateOperations(
routes.flatMap<Promise<GenerateOperationOutput>>((route) => {
return route.methods.map(async (method) => {
const content = generateDocument(
method.summary ?? method.method,
method.description,
ctx.renderer.Root({ baseUrl: ctx.baseUrl }, [
await renderOperation(route.path, method, ctx, true),
await renderOperation(route.path, method, ctx, false),
]),
options,
{
title: method.summary ?? method.method,
description: method.description,
context: {
type: 'operation',
endpoint: method,
route,
},
},
);
if (!method.operationId)
@@ -124,10 +165,17 @@ export async function generateTags(
return {
tag,
content: generateDocument(
idToTitle(tag),
info?.description,
ctx.renderer.Root({ baseUrl: ctx.baseUrl }, child),
options,
{
title: idToTitle(tag),
description: info?.description,
context: {
type: 'tag',
tag: info,
routes,
},
},
),
} satisfies GenerateTagOutput;
}),
+4 -3
View File
@@ -26,7 +26,7 @@ export async function renderOperation(
path: string,
method: MethodInformation,
ctx: RenderContext,
noTitle = false,
hasHead = true,
): Promise<string> {
let level = 2;
const body = noRef(method.requestBody);
@@ -34,7 +34,7 @@ export async function renderOperation(
const info: string[] = [];
const example: string[] = [];
if (!noTitle) {
if (hasHead) {
info.push(
heading(
level,
@@ -43,8 +43,9 @@ export async function renderOperation(
),
);
level++;
if (method.description) info.push(p(method.description));
}
if (method.description) info.push(p(method.description));
info.push(renderPlayground(path, method, ctx));
@@ -1,18 +1,31 @@
import { dump } from 'js-yaml';
import { defaultRenderer } from '@/render/renderer';
import { type GenerateOptions } from '@/generate';
import type { DocumentContext, GenerateOptions } from '@/generate';
export function generateDocument(
title: string,
description: string | undefined,
content: string,
options: GenerateOptions,
frontmatter: {
title: string;
description?: string;
context: DocumentContext;
},
): string {
const banner = dump({
title,
description,
title: frontmatter.title,
description: frontmatter.description,
full: true,
...options.frontmatter?.(title, description),
...(frontmatter.context.type === 'operation'
? {
method: frontmatter.context.endpoint.method,
route: frontmatter.context.route.path,
}
: undefined),
...options.frontmatter?.(
frontmatter.title,
frontmatter.description,
frontmatter.context,
),
}).trim();
const finalImports = (