OpenAPI: Add Source API methods & Dynamic Source support to server

This commit is contained in:
Fuma Nama
2026-05-18 19:42:18 +08:00
parent 56fb65dfe1
commit 975b530af3
21 changed files with 446 additions and 466 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Add Source API methods & Dynamic Source support to OpenAPI server
@@ -115,7 +115,6 @@ You can also use it without generating real files by integrating into [Loader AP
```ts title="lib/source.ts"
import { loader } from 'fumadocs-core/source';
import { openapiPlugin, openapiSource } from 'fumadocs-openapi/server';
import { docs } from 'collections/server';
import { openapi } from '@/lib/openapi';
@@ -123,19 +122,19 @@ export const source = loader(
// [!code ++:6]
{
docs: docs.toFumadocsSource(),
openapi: await openapiSource(openapi, {
openapi: await openapi.staticSource({
baseDir: 'openapi',
}),
},
{
baseUrl: '/docs',
plugins: [openapiPlugin()],
plugins: [openapi.loaderPlugin()],
// ...
},
);
```
`openapiSource()` is a server-side API that generates pages directly to your `loader()`, hence it allows dynamic content generation, such as re-generating page tree as schema changes.
`staticSource()` is a server-side API that generates pages directly to your `loader()`, hence it allows dynamic content generation, such as re-generating page tree as schema changes.
**It will change the type of your pages**, make sure to update all references to your `source`. For example, in your page component:
@@ -62,7 +62,6 @@ You can generate pages dynamically by integrating into [Loader API](/docs/headle
```ts title="lib/source.ts"
import { loader } from 'fumadocs-core/source';
import { openapiPlugin, openapiSource } from 'fumadocs-openapi/server';
import { docs } from 'collections/server';
import { openapi } from '@/lib/openapi';
@@ -70,13 +69,13 @@ export const source = loader(
// [!code ++:6]
{
docs: docs.toFumadocsSource(),
openapi: await openapiSource(openapi, {
openapi: await openapi.staticSource({
baseDir: 'openapi',
}),
},
{
baseUrl: '/docs',
plugins: [openapiPlugin()],
plugins: [openapi.loaderPlugin()],
// ...
},
);
@@ -84,7 +83,7 @@ export const source = loader(
### Update References to `source` [step]
`openapiSource()` is a server-side API that generates pages directly to your `loader()`, hence allowing dynamic generation (e.g. different page tree as schema changes).
`staticSource()` is a server-side API that generates pages directly to your `loader()`, hence allowing dynamic generation (e.g. different page tree as schema changes).
But **it will change the type of your pages**, explicit handling of OpenAPI pages is necessary.
@@ -13,7 +13,6 @@ Pass a record of source objects instead
```ts
import { loader } from 'fumadocs-core/source';
import { openapiPlugin, openapiSource } from 'fumadocs-openapi/server';
import { blog, docs } from 'collections/server';
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
+2 -3
View File
@@ -1,5 +1,4 @@
import { type LoaderPlugin, loader } from 'fumadocs-core/source';
import { openapiPlugin, openapiSource } from 'fumadocs-openapi/server';
import { blog as blogPosts, docs } from 'collections/server';
import { toFumadocsSource } from 'fumadocs-mdx/runtime/server';
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
@@ -8,7 +7,7 @@ import { openapi } from '@/lib/openapi';
export const source = loader(
{
docs: docs.toFumadocsSource(),
openapi: await openapiSource(openapi, {
openapi: await openapi.staticSource({
baseDir: 'openapi/(generated)',
meta: {
folderStyle: 'separator',
@@ -18,7 +17,7 @@ export const source = loader(
},
{
baseUrl: '/docs',
plugins: [pageTreeCodeTitles(), lucideIconsPlugin(), openapiPlugin()],
plugins: [pageTreeCodeTitles(), lucideIconsPlugin(), openapi.loaderPlugin()],
},
);
+2 -3
View File
@@ -1,17 +1,16 @@
import { loader } from 'fumadocs-core/source';
import { openapiPlugin, openapiSource } from 'fumadocs-openapi/server';
import { docs } from 'collections/server';
import { openapi } from './openapi';
export const source = loader(
{
docs: docs.toFumadocsSource(),
openapi: await openapiSource(openapi, {
openapi: await openapi.staticSource({
groupBy: 'tag',
}),
},
{
baseUrl: '/docs',
plugins: [openapiPlugin()],
plugins: [openapi.loaderPlugin()],
},
);
@@ -2,19 +2,18 @@ import { loader } from 'fumadocs-core/source';
import { docs } from 'collections/server';
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
import { docsRoute } from './shared';
import { openapiPlugin, openapiSource } from 'fumadocs-openapi/server';
import { openapi } from './openapi';
export const source = loader(
{
docs: docs.toFumadocsSource(),
openapi: await openapiSource(openapi, {
openapi: await openapi.staticSource({
baseDir: 'openapi',
}),
},
{
baseUrl: docsRoute,
plugins: [lucideIconsPlugin(), openapiPlugin()],
plugins: [lucideIconsPlugin(), openapi.loaderPlugin()],
},
);
-89
View File
@@ -1,89 +0,0 @@
import { createProxy } from '@/server/proxy';
import { processDocument, type ProcessedDocument } from '@/utils/document/process';
import type { Document } from '@/types';
import type { InlineCodeUsageGenerator } from '@/requests/generators';
import fs from 'node:fs';
/**
* schema id -> file path, URL, or downloaded schema object
*/
type SchemaMap = Record<string, string | Document>;
type ProcessedSchemaMap = Record<string, ProcessedDocument>;
export interface OpenAPIOptions {
/**
* Schema files, can be:
* - URL
* - file path
* - a function returning records of downloaded schemas.
*/
input?: string[] | (() => SchemaMap | Promise<SchemaMap>);
disableCache?: boolean;
/**
* The url of proxy to avoid CORS issues
*/
proxyUrl?: string;
}
export interface OpenAPIServer {
createProxy: typeof createProxy;
getSchemas: () => Promise<ProcessedSchemaMap>;
getSchema: (document: string) => Promise<ProcessedDocument>;
/** @private internal API */
_getWatchPaths: () => string[];
readonly options: OpenAPIOptions;
}
export function createOpenAPI(options: OpenAPIOptions = {}): OpenAPIServer {
const { input = [], disableCache = false } = options;
let schemas: Promise<ProcessedSchemaMap> | undefined;
async function getSchemas(): Promise<ProcessedSchemaMap> {
if (Array.isArray(input)) {
const entries = await Promise.all(
input.map(async (item) => [item, await processDocument(item)]),
);
return Object.fromEntries(entries);
} else {
const entries = await Promise.all(
Object.entries(await input()).map(async ([k, v]) => [k, await processDocument(v)]),
);
return Object.fromEntries(entries);
}
}
return {
options,
createProxy,
_getWatchPaths() {
const keys = Array.isArray(input) ? input : Object.keys(input);
return keys.filter((key) => !URL.canParse(key) && fs.existsSync(key));
},
async getSchema(document) {
const schemas = await this.getSchemas();
if (document in schemas) return schemas[document];
console.warn(
`[Fumadocs OpenAPI] the document "${document}" is not listed in the input array, this may not be expected.`,
);
// do not cache unlisted documents
return processDocument(document);
},
async getSchemas() {
if (disableCache) return getSchemas();
return (schemas ??= getSchemas());
},
};
}
/**
* @deprecated
*/
export function createCodeSample<T>(
options: InlineCodeUsageGenerator<T>,
): InlineCodeUsageGenerator<T> {
return options;
}
-2
View File
@@ -1,2 +0,0 @@
export * from './create';
export * from './source-api';
+341
View File
@@ -0,0 +1,341 @@
import { createProxy } from '@/server/proxy';
import { processDocument, type ProcessedDocument } from '@/utils/document/process';
import type { Document } from '@/types';
import type { InlineCodeUsageGenerator } from '@/requests/generators';
import fs from 'node:fs';
import {
type DynamicSource,
type LoaderPlugin,
type PageData,
type PageTreeTransformer,
PathUtils,
type MetaData,
type Source,
type VirtualFile,
} from 'fumadocs-core/source';
import {
getPageProps,
type OperationOutput,
type OutputEntry,
type PageOutput,
type WebhookOutput,
} from '@/utils/pages/builder';
import { toStaticData } from '@/utils/pages/to-static-data';
import path from 'node:path';
import type { DereferencedDocument } from '@/utils/document/dereference';
import type { ApiPageProps } from '@/ui/api-page';
import type { ClientApiPageProps } from '@/ui/create-client';
import type { StructuredData } from 'fumadocs-core/mdx-plugins/remark-structure';
import type { TOCItemType } from 'fumadocs-core/toc';
import type { SchemaToPagesOptions } from '@/utils/pages/preset-auto';
import { MethodLabel } from '@/ui/components/method-label';
/**
* schema id -> file path, URL, or downloaded schema object
*/
type SchemaMap = Record<string, string | Document>;
type ProcessedSchemaMap = Record<string, ProcessedDocument>;
export interface OpenAPIOptions {
/**
* Schema files, can be:
* - URL
* - file path
* - a function returning records of downloaded schemas.
*/
input?: string[] | (() => SchemaMap | Promise<SchemaMap>);
disableCache?: boolean;
/**
* The url of proxy to avoid CORS issues
*/
proxyUrl?: string;
}
export interface OpenAPIServer {
createProxy: typeof createProxy;
getSchemas: () => Promise<ProcessedSchemaMap>;
getSchema: (document: string) => Promise<ProcessedDocument>;
readonly options: OpenAPIOptions;
/**
* Generate virtual pages for Fumadocs Source API
*/
staticSource: (
options?: OpenAPISourceOptions,
) => Promise<Source<{ metaData: MetaData; pageData: OpenAPIPageData }>>;
/**
* Generate virtual pages for Fumadocs Source API
*/
dynamicSource: (
options?: OpenAPISourceOptions,
) => DynamicSource<{ metaData: MetaData; pageData: OpenAPIPageData }>;
/**
* Fumadocs Source API integration, pass this to `plugins` array in `loader()`.
*/
loaderPlugin: () => LoaderPlugin;
/** @private internal API */
_getWatchPaths: () => string[];
}
export interface OpenAPIPageData extends PageData {
getAPIPageProps: () => ApiPageProps;
getSchema: () => { id: string } & DereferencedDocument;
getClientAPIPageProps: () => Promise<ClientApiPageProps>;
structuredData: StructuredData;
toc: TOCItemType[];
}
export type OpenAPISourceOptions = SchemaToPagesOptions & {
baseDir?: string;
/** Generate `meta.json` files */
meta?: boolean | { folderStyle?: 'folder' | 'separator' };
};
export function createOpenAPI(options: OpenAPIOptions = {}): OpenAPIServer {
const { input = [], disableCache = false } = options;
let schemas: Promise<ProcessedSchemaMap> | undefined;
async function getSchemas(): Promise<ProcessedSchemaMap> {
if (Array.isArray(input)) {
const entries = await Promise.all(
input.map(async (item) => [item, await processDocument(item)]),
);
return Object.fromEntries(entries);
} else {
const entries = await Promise.all(
Object.entries(await input()).map(async ([k, v]) => [k, await processDocument(v)]),
);
return Object.fromEntries(entries);
}
}
async function getVirtualFiles(server: OpenAPIServer, options: OpenAPISourceOptions) {
const { baseDir = '', meta = false } = options;
const { createAutoPreset } = await import('@/utils/pages/preset-auto');
const { fromServer } = await import('@/utils/pages/builder');
const files: VirtualFile<{
pageData: OpenAPIPageData;
metaData: MetaData;
}>[] = [];
const entries = await fromServer(server, createAutoPreset(options));
for (const [schemaId, list] of Object.entries(entries)) {
const processed = await server.getSchema(schemaId);
function onEntry(entry: PageOutput | OperationOutput | WebhookOutput) {
const props = getPageProps(entry);
files.push({
type: 'page',
path: `${baseDir}/${entry.path}`,
data: {
...entry.info,
getAPIPageProps() {
return props;
},
async getClientAPIPageProps() {
return {
payload: {
bundled: processed.bundled,
proxyUrl: server.options.proxyUrl,
},
...props,
};
},
getSchema() {
return {
id: schemaId,
...processed,
};
},
...toStaticData(props, processed.dereferenced),
_openapi: {
method:
entry.type === 'operation' || entry.type === 'webhook'
? entry.item.method
: undefined,
webhook: entry.type === 'webhook',
deprecated: entry.info.deprecated,
},
},
});
}
function onEntries(entries: OutputEntry[], parent?: OutputEntry) {
if (!meta) {
for (const entry of entries) {
if (entry.type === 'group') {
onEntries(entry.entries, entry);
} else {
onEntry(entry);
}
}
return;
}
const { folderStyle = 'folder' } = meta === true ? {} : meta;
const pages: string[] = [];
for (const entry of entries) {
const relativePath = PathUtils.slash(
parent ? path.relative(parent.path, entry.path) : entry.path,
);
if (entry.type === 'group') {
onEntries(entry.entries, entry);
if (folderStyle === 'folder') {
pages.push(relativePath);
} else {
pages.push(`---${entry.info.title}---`, `...${relativePath}`);
}
} else {
onEntry(entry);
pages.push(relativePath.slice(0, -path.extname(entry.path).length));
}
}
if (pages.length === 0) return;
files.push({
type: 'meta',
path: path.join(baseDir, parent?.path ?? '', 'meta.json'),
data: {
title: parent?.info.title,
description: parent?.info.description,
pages,
},
});
}
onEntries(list);
}
return files;
}
return {
options,
createProxy,
_getWatchPaths() {
const keys = Array.isArray(input) ? input : Object.keys(input);
return keys.filter((key) => !URL.canParse(key) && fs.existsSync(key));
},
async getSchema(document) {
const schemas = await this.getSchemas();
if (document in schemas) return schemas[document];
console.warn(
`[Fumadocs OpenAPI] the document "${document}" is not listed in the input array, this may not be expected.`,
);
// do not cache unlisted documents
return processDocument(document);
},
async getSchemas() {
if (disableCache) return getSchemas();
return (schemas ??= getSchemas());
},
async staticSource(options = {}) {
return {
files: await getVirtualFiles(this, options),
};
},
dynamicSource(options = {}) {
return {
files: () => getVirtualFiles(this, options),
};
},
loaderPlugin() {
return openapiPlugin();
},
};
}
/**
* @deprecated
*/
export function createCodeSample<T>(
options: InlineCodeUsageGenerator<T>,
): InlineCodeUsageGenerator<T> {
return options;
}
declare module 'fumadocs-core/source' {
export interface PageData {
/**
* Added by Fumadocs OpenAPI
*/
_openapi?: InternalOpenAPIMeta;
}
}
export interface InternalOpenAPIMeta {
method?: string;
webhook?: boolean;
deprecated?: boolean;
}
/**
* Fumadocs Source API integration, pass this to `plugins` array in `loader()`.
*/
export function openapiPlugin(): LoaderPlugin {
return {
name: 'fumadocs:openapi',
enforce: 'pre',
transformPageTree: {
file(node, filePath) {
if (!filePath) return node;
const file = this.storage.read(filePath);
if (!file || file.format !== 'page') return node;
const openApiData = file.data._openapi;
if (!openApiData || typeof openApiData !== 'object') return node;
if (openApiData.deprecated) {
node.name = <span className="fd-page-tree-item-name line-through">{node.name}</span>;
}
if (openApiData.webhook) {
node.name = (
<>
{node.name}{' '}
<span className="ms-auto border border-current px-1 rounded-lg text-xs text-nowrap font-mono">
Webhook
</span>
</>
);
} else if (openApiData.method) {
node.name = (
<>
{node.name}{' '}
<MethodLabel className="ms-auto text-xs text-nowrap">
{openApiData.method}
</MethodLabel>
</>
);
}
return node;
},
},
};
}
/**
* Generate virtual pages for Fumadocs Source API
*/
export async function openapiSource(server: OpenAPIServer, options: OpenAPISourceOptions = {}) {
return server.staticSource(options);
}
/**
* @deprecated use `openapiPlugin()`
*/
export function transformerOpenAPI(): PageTreeTransformer {
return openapiPlugin().transformPageTree!;
}
-247
View File
@@ -1,247 +0,0 @@
import { MethodLabel } from '@/ui/components/method-label';
import {
PathUtils,
type Source,
type LoaderPlugin,
type MetaData,
type PageData,
type PageTreeTransformer,
type VirtualFile,
} from 'fumadocs-core/source';
import type { OpenAPIServer } from '@/server/create';
import type { SchemaToPagesOptions } from '@/utils/pages/preset-auto';
import type { ApiPageProps } from '@/ui/api-page';
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
import type { TOCItemType } from 'fumadocs-core/toc';
import type { DereferencedDocument } from '@/utils/document/dereference';
import type {
OperationOutput,
OutputEntry,
PageOutput,
WebhookOutput,
} from '@/utils/pages/builder';
import path from 'node:path';
import type { ClientApiPageProps } from '@/ui/create-client';
import { toStaticData } from '@/utils/pages/to-static-data';
declare module 'fumadocs-core/source' {
export interface PageData {
/**
* Added by Fumadocs OpenAPI
*/
_openapi?: InternalOpenAPIMeta;
}
}
export interface InternalOpenAPIMeta {
method?: string;
webhook?: boolean;
deprecated?: boolean;
}
/**
* Fumadocs Source API integration, pass this to `plugins` array in `loader()`.
*/
export function openapiPlugin(): LoaderPlugin {
return {
name: 'fumadocs:openapi',
enforce: 'pre',
transformPageTree: {
file(node, filePath) {
if (!filePath) return node;
const file = this.storage.read(filePath);
if (!file || file.format !== 'page') return node;
const openApiData = file.data._openapi;
if (!openApiData || typeof openApiData !== 'object') return node;
if (openApiData.deprecated) {
node.name = <span className="fd-page-tree-item-name line-through">{node.name}</span>;
}
if (openApiData.webhook) {
node.name = (
<>
{node.name}{' '}
<span className="ms-auto border border-current px-1 rounded-lg text-xs text-nowrap font-mono">
Webhook
</span>
</>
);
} else if (openApiData.method) {
node.name = (
<>
{node.name}{' '}
<MethodLabel className="ms-auto text-xs text-nowrap">
{openApiData.method}
</MethodLabel>
</>
);
}
return node;
},
},
};
}
export interface OpenAPIPageData extends PageData {
getAPIPageProps: () => ApiPageProps;
getSchema: () => { id: string } & DereferencedDocument;
getClientAPIPageProps: () => Promise<ClientApiPageProps>;
structuredData: StructuredData;
toc: TOCItemType[];
}
export type OpenAPISourceOptions = SchemaToPagesOptions & {
baseDir?: string;
/** Generate `meta.json` files */
meta?: boolean | { folderStyle?: 'folder' | 'separator' };
};
/**
* Generate virtual pages for Fumadocs Source API
*/
export async function openapiSource(
server: OpenAPIServer,
options: OpenAPISourceOptions = {},
): Promise<
Source<{
metaData: MetaData;
pageData: OpenAPIPageData;
}>
> {
const { baseDir = '', meta = false } = options;
const { createAutoPreset } = await import('@/utils/pages/preset-auto');
const { fromServer } = await import('@/utils/pages/builder');
const files: VirtualFile<{
pageData: OpenAPIPageData;
metaData: MetaData;
}>[] = [];
const entries = await fromServer(server, createAutoPreset(options));
for (const [schemaId, list] of Object.entries(entries)) {
const processed = await server.getSchema(schemaId);
function onEntry(entry: PageOutput | OperationOutput | WebhookOutput) {
const props = getProps(entry);
files.push({
type: 'page',
path: `${baseDir}/${entry.path}`,
data: {
...entry.info,
getAPIPageProps() {
return props;
},
async getClientAPIPageProps() {
return {
payload: {
bundled: processed.bundled,
proxyUrl: server.options.proxyUrl,
},
...props,
};
},
getSchema() {
return {
id: schemaId,
...processed,
};
},
...toStaticData(props, processed.dereferenced),
_openapi: {
method:
entry.type === 'operation' || entry.type === 'webhook'
? entry.item.method
: undefined,
webhook: entry.type === 'webhook',
deprecated: entry.info.deprecated,
},
},
});
}
function onEntries(entries: OutputEntry[], parent?: OutputEntry) {
if (!meta) {
for (const entry of entries) {
if (entry.type === 'group') {
onEntries(entry.entries, entry);
} else {
onEntry(entry);
}
}
return;
}
const { folderStyle = 'folder' } = meta === true ? {} : meta;
const pages: string[] = [];
for (const entry of entries) {
const relativePath = PathUtils.slash(
parent ? path.relative(parent.path, entry.path) : entry.path,
);
if (entry.type === 'group') {
onEntries(entry.entries, entry);
if (folderStyle === 'folder') {
pages.push(relativePath);
} else {
pages.push(`---${entry.info.title}---`, `...${relativePath}`);
}
} else {
onEntry(entry);
pages.push(relativePath.slice(0, -PathUtils.extname(entry.path).length));
}
}
if (pages.length === 0) return;
files.push({
type: 'meta',
path: path.join(baseDir, parent?.path ?? '', 'meta.json'),
data: {
title: parent?.info.title,
description: parent?.info.description,
pages,
},
});
}
onEntries(list);
}
return {
files,
};
}
function getProps(entry: PageOutput | OperationOutput | WebhookOutput): ApiPageProps {
if (entry.type === 'operation')
return {
document: entry.schemaId,
operations: [entry.item],
showDescription: true,
};
if (entry.type === 'webhook')
return {
document: entry.schemaId,
webhooks: [entry.item],
showDescription: true,
};
return {
showTitle: true,
showDescription: true,
document: entry.schemaId,
operations: entry.operations,
webhooks: entry.webhooks,
};
}
/**
* @deprecated use `openapiPlugin()`
*/
export function transformerOpenAPI(): PageTreeTransformer {
return openapiPlugin().transformPageTree!;
}
@@ -35,15 +35,7 @@ export function Badge({
...props
}: Omit<HTMLAttributes<HTMLSpanElement>, 'color'> & VariantProps<typeof badgeVariants>) {
return (
<span
className={cn(
badgeVariants({
color,
className,
}),
)}
{...props}
>
<span className={cn(badgeVariants({ color }), className)} {...props}>
{props.children}
</span>
);
+24 -1
View File
@@ -1,6 +1,6 @@
import type { DereferencedDocument } from '@/utils/document/dereference';
import type { OpenAPIServer } from '@/server';
import type { OperationItem, WebhookItem } from '@/ui/api-page';
import type { ApiPageProps, OperationItem, WebhookItem } from '@/ui/api-page';
import type { OperationObject, PathItemObject, TagObject } from '@/types';
import { getTagDisplayName, methodKeys, type NoReference } from '@/utils/schema';
import { idToTitle } from '@/utils/id-to-title';
@@ -227,3 +227,26 @@ export function fromSchema(
return files;
}
export function getPageProps(entry: PageOutput | OperationOutput | WebhookOutput): ApiPageProps {
if (entry.type === 'operation')
return {
document: entry.schemaId,
operations: [entry.item],
showDescription: true,
};
if (entry.type === 'webhook')
return {
document: entry.schemaId,
webhooks: [entry.item],
showDescription: true,
};
return {
showTitle: true,
showDescription: true,
document: entry.schemaId,
operations: entry.operations,
webhooks: entry.webhooks,
};
}
+49 -98
View File
@@ -3,7 +3,12 @@ import type { DereferencedDocument } from '@/utils/document/dereference';
import type { TagObject } from '@/types';
import { dump } from 'js-yaml';
import { removeUndefined } from '@/utils/remove-undefined';
import type { OperationOutput, PageOutput, WebhookOutput } from '@/utils/pages/builder';
import {
getPageProps,
type OperationOutput,
type PageOutput,
type WebhookOutput,
} from '@/utils/pages/builder';
import type { InternalOpenAPIMeta } from '@/server/source-api';
import { toStaticData } from '@/utils/pages/to-static-data';
import { doubleQuote } from '@/requests/string-utils';
@@ -31,7 +36,7 @@ export interface PagesToTextOptions {
/**
* Add description to document body.
*
* We recommend but don't enable it by default because some OpenAPI schemas have invalid description that breaks MDX syntax.
* We recommend to enable it, it is disabled by default because some OpenAPI schemas have invalid description that breaks Markdown syntax.
*
* @defaultValue false
*/
@@ -53,40 +58,49 @@ export function toText(
processed: DereferencedDocument,
options: PagesToTextOptions = {},
) {
switch (entry.type) {
case 'operation':
return generatePage(
entry.schemaId,
processed,
{
operations: [entry.item],
},
options,
entry,
);
case 'page':
return generatePage(
entry.schemaId,
processed,
{
operations: entry.operations,
webhooks: entry.webhooks,
showTitle: true,
},
options,
entry,
);
case 'webhook':
return generatePage(
entry.schemaId,
processed,
{
webhooks: [entry.item],
},
options,
entry,
);
const { frontmatter, includeDescription = false } = options;
const extend = frontmatter?.(
entry.info.title,
entry.info.description,
entry.type === 'page'
? entry.tag
? { type: 'tag', tag: entry.tag }
: { type: 'file' }
: { type: 'operation' },
);
const pageProps = getPageProps(entry);
if (!includeDescription) {
pageProps.showDescription = false;
}
let meta: InternalOpenAPIMeta | undefined;
if (entry.type === 'operation' || entry.type === 'webhook') {
const operation = entry.item;
meta = {
method: operation.method.toUpperCase(),
webhook: entry.type === 'webhook',
deprecated: entry.info.deprecated,
};
}
const data = toStaticData(pageProps, processed.dereferenced);
return generateDocument(
{
title: entry.info.title,
description: !includeDescription ? entry.info.description : undefined,
full: true,
...extend,
_openapi: {
...meta,
...data,
...(extend?._openapi as object | undefined),
},
},
pageContent(pageProps),
options,
);
}
export function generateDocument(
@@ -135,69 +149,6 @@ export type DocumentContext =
type: 'file';
};
function generatePage(
schemaId: string,
processed: DereferencedDocument,
pageProps: Omit<ApiPageProps, 'document'>,
options: PagesToTextOptions,
entry: PageOutput | OperationOutput | WebhookOutput,
): string {
const { frontmatter, includeDescription = false } = options;
const extend = frontmatter?.(
entry.info.title,
entry.info.description,
entry.type === 'page'
? entry.tag
? { type: 'tag', tag: entry.tag }
: { type: 'file' }
: { type: 'operation' },
);
const page: ApiPageProps = {
...pageProps,
document: schemaId,
};
let meta: InternalOpenAPIMeta | undefined;
if (page.operations?.length === 1) {
const operation = page.operations[0];
meta = {
method: operation.method.toUpperCase(),
deprecated: entry.info.deprecated,
};
} else if (page.webhooks?.length === 1) {
const webhook = page.webhooks[0];
meta = {
method: webhook.method.toUpperCase(),
webhook: true,
deprecated: entry.info.deprecated,
};
}
const data = toStaticData(page, processed.dereferenced);
const content: string[] = [];
if (entry.info.description && includeDescription) content.push(entry.info.description);
content.push(pageContent(page));
return generateDocument(
{
title: entry.info.title,
description: !includeDescription ? entry.info.description : undefined,
full: true,
...extend,
_openapi: {
...meta,
...data,
...(extend?._openapi as object | undefined),
},
},
content.join('\n\n'),
options,
);
}
function pageContent({
showTitle,
showDescription,
@@ -58,7 +58,6 @@ title: Operations
description: Operational information about the museum.
full: true
_openapi:
method: GET
toc:
- depth: 2
title: Get museum hours
@@ -4,6 +4,7 @@ title: Create a pet
full: true
_openapi:
method: POST
webhook: false
toc: []
structuredData:
headings: []
@@ -21,6 +22,7 @@ title: List all pets
full: true
_openapi:
method: GET
webhook: false
toc: []
structuredData:
headings: []
@@ -38,6 +40,7 @@ title: Info for a specific pet
full: true
_openapi:
method: GET
webhook: false
toc: []
structuredData:
headings: []
@@ -5,6 +5,7 @@ description: Retrieve inventory details for a specific product.
full: true
_openapi:
method: GET
webhook: false
toc: []
structuredData:
headings: []
@@ -24,6 +25,7 @@ description: Retrieve inventory details for a specific product.
full: true
_openapi:
method: GET
webhook: false
toc: []
structuredData:
headings: []
@@ -43,6 +45,7 @@ description: Retrieve details of a product by its ID.
full: true
_openapi:
method: GET
webhook: false
toc: []
structuredData:
headings: []
@@ -19,6 +19,7 @@ description: Retrieve inventory details for a specific product.
full: true
_openapi:
method: GET
webhook: false
toc: []
structuredData:
headings: []
@@ -38,6 +39,7 @@ description: Retrieve details of a product by its ID.
full: true
_openapi:
method: GET
webhook: false
toc: []
structuredData:
headings: []
@@ -5,6 +5,7 @@ description: Retrieve inventory details for a specific product.
full: true
_openapi:
method: GET
webhook: false
toc: []
structuredData:
headings: []
@@ -43,6 +44,7 @@ description: Retrieve inventory details for a specific product.
full: true
_openapi:
method: GET
webhook: false
toc: []
structuredData:
headings: []
@@ -62,6 +64,7 @@ description: Retrieve details of a product by its ID.
full: true
_openapi:
method: GET
webhook: false
toc: []
structuredData:
headings: []
@@ -5,6 +5,7 @@ description: Retrieve inventory details for a specific product.
full: true
_openapi:
method: GET
webhook: false
toc: []
structuredData:
headings: []
@@ -24,6 +25,7 @@ description: Retrieve details of a product by its ID.
full: true
_openapi:
method: GET
webhook: false
toc: []
structuredData:
headings: []
+1 -1
View File
@@ -14,7 +14,7 @@ export default defineConfig({
'./src/ui/client/index.tsx',
'./src/playground/client.tsx',
'./src/scalar/index.tsx',
'./src/server/index.ts',
'./src/server/index.tsx',
'./src/requests/generators/*.ts',
],
unbundle: true,