OpenAPI: Support structuredData in virtual file source

This commit is contained in:
Fuma Nama
2025-12-20 16:25:41 +08:00
parent c90a5945db
commit 058a46b59f
6 changed files with 102 additions and 75 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Support `structuredData` in virtual file source
+31 -21
View File
@@ -10,6 +10,9 @@ import type {
import type { OpenAPIServer } from '@/server/create';
import type { SchemaToPagesOptions } from '@/utils/pages/preset-auto';
import type { ApiPageProps } from '@/ui/api-page';
import { toStaticData } from '@/utils/pages/to-static-data';
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
import type { TOCItemType } from 'fumadocs-core/toc';
declare module 'fumadocs-core/source' {
export interface PageData {
@@ -69,13 +72,15 @@ export function openapiPlugin(): LoaderPlugin {
interface OpenAPIPageData extends PageData {
getAPIPageProps: () => ApiPageProps;
structuredData: StructuredData;
toc: TOCItemType[];
}
/**
* Generate virtual pages for Fumadocs Source API
*/
export async function openapiSource(
from: OpenAPIServer,
server: OpenAPIServer,
options: SchemaToPagesOptions & {
baseDir?: string;
} = {},
@@ -94,27 +99,32 @@ export async function openapiSource(
metaData: MetaData;
}>[] = [];
const entries = await fromServer(from, createAutoPreset(options));
for (const entry of Object.values(entries).flat()) {
files.push({
type: 'page',
path: `${baseDir}/${entry.path}`,
data: {
...entry.info,
getAPIPageProps() {
const props = toBody(entry);
props.showDescription ??= true;
return props;
const entries = await fromServer(server, createAutoPreset(options));
for (const [schemaId, list] of Object.entries(entries)) {
const dereferenced = (await server.getSchema(schemaId)).dereferenced;
for (const entry of list) {
const props = toBody(entry);
props.showDescription ??= true;
files.push({
type: 'page',
path: `${baseDir}/${entry.path}`,
data: {
...entry.info,
getAPIPageProps() {
return props;
},
...toStaticData(props, dereferenced),
_openapi: {
method:
entry.type === 'operation' || entry.type === 'webhook'
? entry.item.method
: undefined,
webhook: entry.type === 'webhook',
},
},
_openapi: {
method:
entry.type === 'operation' || entry.type === 'webhook'
? entry.item.method
: undefined,
webhook: entry.type === 'webhook',
},
},
});
});
}
}
return {
+1 -1
View File
@@ -102,7 +102,7 @@ export interface CreateAPIPageOptions {
* @param generators - codegens for API example usages
*/
renderAPIExampleUsageTabs?: (
generators: CodeUsageGenerator<unknown>[],
generators: CodeUsageGenerator[],
ctx: RenderContext,
) => Awaitable<ReactNode>;
@@ -0,0 +1,60 @@
import type { NoReference } from '@/utils/schema';
import type { Document, OperationObject } from '@/types';
import Slugger from 'github-slugger';
import { idToTitle } from '@/utils/id-to-title';
import type { TOCItemType } from 'fumadocs-core/toc';
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
import type { ApiPageProps } from '@/ui';
export function toStaticData(
page: ApiPageProps,
dereferenced: NoReference<Document>,
): {
toc: TOCItemType[];
structuredData: StructuredData;
} {
const slugger = new Slugger();
const toc: TOCItemType[] = [];
const structuredData: StructuredData = { headings: [], contents: [] };
function pathItem(item: NoReference<OperationObject>, defaultTitle: string) {
if (page.showTitle && item.operationId) {
const title =
item.summary ||
(item.operationId ? idToTitle(item.operationId) : defaultTitle);
const id = slugger.slug(title);
toc.push({
depth: 2,
title,
url: `#${id}`,
});
structuredData.headings.push({
content: title,
id,
});
}
if (item.description)
structuredData.contents.push({
content: item.description,
heading: structuredData.headings.at(-1)?.id,
});
}
for (const item of page.operations ?? []) {
const operation = dereferenced.paths?.[item.path]?.[item.method];
if (!operation) continue;
pathItem(operation, item.path);
}
for (const item of page.webhooks ?? []) {
const webhook = dereferenced.webhooks?.[item.name]?.[item.method];
if (!webhook) continue;
pathItem(webhook, item.name);
}
return { toc, structuredData };
}
+3 -51
View File
@@ -1,15 +1,11 @@
import { idToTitle } from '@/utils/id-to-title';
import type { ApiPageProps, OperationItem, WebhookItem } from '@/ui/api-page';
import type { ProcessedDocument } from '@/utils/process-document';
import type { TableOfContents } from 'fumadocs-core/toc';
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
import type { Document, TagObject } from '@/types';
import type { TagObject } from '@/types';
import { dump } from 'js-yaml';
import type { NoReference } from '@/utils/schema';
import Slugger from 'github-slugger';
import { removeUndefined } from '@/utils/remove-undefined';
import type { OutputEntry } from '@/utils/pages/builder';
import type { InternalOpenAPIMeta } from '@/server/source-api';
import { toStaticData } from '@/utils/pages/to-static-data';
export interface PagesToTextOptions {
/**
@@ -162,11 +158,6 @@ export function generateDocument(
return out.join('\n\n');
}
interface StaticData {
toc: TableOfContents;
structuredData: StructuredData;
}
export type DocumentContext =
| {
type: 'tag';
@@ -212,7 +203,7 @@ function generatePage(
};
}
const data = generateStaticData(processed.dereferenced, page);
const data = toStaticData(page, processed.dereferenced);
const content: string[] = [];
if (options.description && includeDescription)
@@ -236,45 +227,6 @@ function generatePage(
);
}
function generateStaticData(
dereferenced: NoReference<Document>,
props: ApiPageProps,
): StaticData {
const slugger = new Slugger();
const toc: TableOfContents = [];
const structuredData: StructuredData = { headings: [], contents: [] };
for (const item of props.operations ?? []) {
const operation = dereferenced.paths?.[item.path]?.[item.method];
if (!operation) continue;
if (props.showTitle && 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({
showTitle,
showDescription,
@@ -40,8 +40,8 @@ export function schemaToString(
if ((flags & FormatFlags.UseAlias) === FormatFlags.UseAlias) {
if (schema.title) return schema.title;
const ref = ctx?.getRawRef(schema);
if (ref) return ref.split('/').at(-1)!;
const ref = ctx?.getRawRef(schema)?.split('/');
if (ref && ref.length > 0) return ref[ref.length - 1];
}
if (Array.isArray(schema.type)) {