OpenAPI: Support generating virtual pages for Source API

This commit is contained in:
Fuma Nama
2025-10-05 00:12:00 +08:00
parent 69b481578f
commit 5cb199fa37
6 changed files with 104 additions and 48 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': minor
---
Support generating virtual pages for Source API
-1
View File
@@ -12,7 +12,6 @@ export {
loadFiles,
type PageFile,
type MetaFile,
type LoadOptions,
type ContentStorage,
type Transformer,
} from './load-files';
+2 -7
View File
@@ -5,11 +5,6 @@ import { basename, dirname, joinPath, slash, splitPath } from '@/source/path';
import type { I18nConfig } from '@/i18n';
import type { LoaderPlugin } from '@/source/plugins';
export interface LoadOptions {
buildFile: (file: VirtualFile) => MetaFile | PageFile;
plugins?: LoaderPlugin[];
}
export type ContentStorage<
Page extends PageData = PageData,
Meta extends MetaData = MetaData,
@@ -71,10 +66,10 @@ const parsers = {
*/
export function loadFiles(
files: VirtualFile[],
options: LoadOptions,
buildFile: (file: VirtualFile) => MetaFile | PageFile,
plugins: LoaderPlugin[],
i18n: I18nConfig,
): Record<string, ContentStorage> {
const { buildFile, plugins = [] } = options;
const parser = parsers[i18n.parser ?? 'dot'];
const storages: Record<string, ContentStorage> = {};
const normalized = files.map((file) =>
+26 -24
View File
@@ -310,7 +310,7 @@ function resolveConfig({
plugins: buildPlugins([
slugsPlugin(slugs),
icon && iconPlugin(icon),
compatPlugin(base, base.pageTree),
compatPlugin(base),
...plugins,
]),
};
@@ -323,10 +323,14 @@ function resolveConfig({
return config;
}
function createOutput(
config: ResolvedLoaderConfig,
): LoaderOutput<LoaderConfig> {
const { source, baseUrl = '/', url: urlFn, i18n } = config;
function createOutput({
source,
baseUrl = '/',
url: urlFn,
i18n,
plugins = [],
pageTree: pageTreeConfig,
}: ResolvedLoaderConfig): LoaderOutput<LoaderConfig> {
const getUrl: UrlFn = urlFn
? (...args) => normalizeUrl(urlFn(...args))
: createGetUrl(baseUrl, i18n);
@@ -335,27 +339,25 @@ function createOutput(
const storages = loadFiles(
files,
{
buildFile(file) {
if (file.type === 'page') {
return {
format: 'page',
path: file.path,
slugs: file.slugs,
data: file.data,
absolutePath: file.absolutePath ?? '',
} as PageFile;
}
(file) => {
if (file.type === 'page') {
return {
format: 'meta',
format: 'page',
path: file.path,
absolutePath: file.absolutePath ?? '',
slugs: file.slugs,
data: file.data,
} as MetaFile;
},
plugins: config.plugins,
absolutePath: file.absolutePath ?? '',
} as PageFile;
}
return {
format: 'meta',
path: file.path,
absolutePath: file.absolutePath ?? '',
data: file.data,
} as MetaFile;
},
plugins,
i18n ?? {
defaultLanguage,
parser: 'none',
@@ -372,8 +374,8 @@ function createOutput(
get pageTree() {
pageTree ??= builder.buildI18n({
storages,
plugins: config.plugins,
...config.pageTree,
plugins,
...pageTreeConfig,
});
return i18n
+12 -14
View File
@@ -17,8 +17,8 @@ export interface LegacyLoaderOptions {
}
export interface LegacyPageTreeOptions<
Page extends PageData,
Meta extends MetaData,
Page extends PageData = PageData,
Meta extends MetaData = MetaData,
> {
/**
* @deprecated use `plugins` instead
@@ -48,20 +48,18 @@ export interface LegacyPageTreeOptions<
/**
* legacy features compatibility layer
*/
export function compatPlugin<Page extends PageData, Meta extends MetaData>(
loader: LegacyLoaderOptions,
pageTreeOptions?: LegacyPageTreeOptions<Page, Meta>,
): LoaderPlugin<Page, Meta> {
export function compatPlugin({
pageTree,
transformers,
}: LegacyLoaderOptions & { pageTree?: LegacyPageTreeOptions }): LoaderPlugin {
return {
name: 'fumadocs:compat',
config(config) {
const plugins = (
config.plugins ? [...config.plugins] : []
) as LoaderPlugin<Page, Meta>[];
const plugins = config.plugins ? [...config.plugins] : [];
if (pageTreeOptions) {
if (pageTree) {
const { attachFile, attachSeparator, attachFolder, transformers } =
pageTreeOptions;
pageTree;
for (const transformer of transformers ?? []) {
plugins.push(fromPageTreeTransformer(transformer));
@@ -103,15 +101,15 @@ export function compatPlugin<Page extends PageData, Meta extends MetaData>(
);
}
if (loader.transformers) {
for (const transformer of loader.transformers) {
if (transformers) {
for (const transformer of transformers) {
plugins.push(fromStorageTransformer(transformer));
}
}
return {
...config,
plugins: plugins as LoaderPlugin[],
plugins,
};
},
};
+59 -2
View File
@@ -2,15 +2,48 @@ import { MethodLabel } from '@/ui/components/method-label';
import type * as PageTree from 'fumadocs-core/page-tree';
import type {
LoaderPlugin,
PageData,
PageFile,
PageTreeTransformer,
Source,
SourceConfig,
} from 'fumadocs-core/source';
import type { OpenAPIServer } from '@/server/create';
import {
type OutputEntry,
type SchemaToPagesOptions,
serverToPages,
} from '@/utils/schema-to-pages';
export type OpenAPIPluginOptions<Page extends PageData = PageData> =
SchemaToPagesOptions & {
from: OpenAPIServer;
baseDir?: string;
onPage: (raw: OutputEntry) => Page;
};
export function openapiPlugin(): LoaderPlugin;
export function openapiPlugin<S extends SourceConfig | Source<SourceConfig>>(
options?: OpenAPIPluginOptions<
S extends Source<infer Config>
? Config['pageData']
: S extends SourceConfig
? S['pageData']
: never
>,
): Promise<LoaderPlugin>;
/**
* Fumadocs Source API integration, pass this to `plugins` array in `loader()`.
*/
export function openapiPlugin(): LoaderPlugin {
return {
export function openapiPlugin(
autoPages?: OpenAPIPluginOptions,
): LoaderPlugin | Promise<LoaderPlugin> {
const basePlugin: LoaderPlugin = {
name: 'fumadocs:openapi',
enforce: 'pre',
transformPageTree: {
file(node, filePath) {
if (!filePath) return node;
@@ -43,6 +76,30 @@ export function openapiPlugin(): LoaderPlugin {
},
},
};
if (autoPages) {
const { from, onPage, baseDir = '' } = autoPages;
return serverToPages(from, autoPages).then((entries) => {
const pages = Object.values(entries).flat();
basePlugin.transformStorage = ({ storage }) => {
for (const page of pages) {
storage.write(`${baseDir}/${page.path}`, {
format: 'page',
data: onPage(page),
path: page.path,
absolutePath: '',
slugs: undefined as unknown as string[],
});
}
};
return basePlugin;
});
}
return basePlugin;
}
/**