fix(openapi): improve loader API types

This commit is contained in:
Fuma Nama
2026-05-27 19:27:48 +08:00
parent 84ce691a06
commit 5d579bdfac
6 changed files with 72 additions and 83 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'fumadocs-openapi': patch
'fumadocs-core': patch
---
improve loader API types
+4 -2
View File
@@ -1,6 +1,8 @@
import type { Awaitable } from '@/types';
import type { DynamicLoader } from './dynamic';
import type { StructuredData } from '@/mdx-plugins';
import z from 'zod';
import { pageSchema } from './schema';
export type SourceUnion<Config extends SourceConfig = SourceConfig> =
| StaticSource<Config>
@@ -39,10 +41,10 @@ export interface MetaData {
export interface PageData {
icon?: string | undefined;
title?: string;
title?: string | undefined;
description?: string | undefined;
structuredData?: StructuredData | (() => Awaitable<StructuredData>);
structuredData?: StructuredData | (() => Awaitable<StructuredData>) | undefined;
}
export type VirtualFile<Config extends SourceConfig = SourceConfig> =
+46 -40
View File
@@ -104,7 +104,11 @@ export function server<Config, TC extends InternalTypeConfig>(options: ServerOpt
_name: Name,
base: string,
glob: AwaitableGlobEntries<unknown>,
) {
): Promise<
Config[Name] extends DocCollection<infer Schema> | DocsCollection<infer Schema>
? DocCollectionEntry<Name, StandardSchemaV1.InferOutput<Schema>, TC>[]
: never
> {
const out = await Promise.all(
Object.entries(glob).map(async ([k, v]) => {
const data: CompiledMDXProperties = typeof v === 'function' ? await v() : v;
@@ -117,18 +121,18 @@ export function server<Config, TC extends InternalTypeConfig>(options: ServerOpt
}),
);
return out as unknown as Config[Name] extends
| DocCollection<infer Schema>
| DocsCollection<infer Schema>
? DocCollectionEntry<Name, StandardSchemaV1.InferOutput<Schema>, TC>[]
: never;
return out as never;
},
async docLazy<Name extends keyof Config & string>(
_name: Name,
base: string,
head: AwaitableGlobEntries<unknown>,
body: Record<string, () => Promise<unknown>>,
) {
): Promise<
Config[Name] extends DocCollection<infer Schema> | DocsCollection<infer Schema>
? AsyncDocCollectionEntry<Name, StandardSchemaV1.InferOutput<Schema>, TC>[]
: never
> {
const out = await Promise.all(
Object.entries(head).map(async ([k, v]) => {
const data = typeof v === 'function' ? await v() : v;
@@ -147,17 +151,19 @@ export function server<Config, TC extends InternalTypeConfig>(options: ServerOpt
}),
);
return out as unknown as Config[Name] extends
| DocCollection<infer Schema>
| DocsCollection<infer Schema>
? AsyncDocCollectionEntry<Name, StandardSchemaV1.InferOutput<Schema>, TC>[]
: never;
return out as never;
},
async meta<Name extends keyof Config & string>(
_name: Name,
base: string,
glob: AwaitableGlobEntries<unknown>,
) {
): Promise<
Config[Name] extends
| MetaCollection<infer Schema>
| DocsCollection<StandardSchemaV1, infer Schema>
? MetaCollectionEntry<StandardSchemaV1.InferOutput<Schema>>[]
: never
> {
const out = await Promise.all(
Object.entries(glob).map(async ([k, v]) => {
const data = typeof v === 'function' ? await v() : v;
@@ -169,11 +175,7 @@ export function server<Config, TC extends InternalTypeConfig>(options: ServerOpt
}),
);
return out as unknown as Config[Name] extends
| MetaCollection<infer Schema>
| DocsCollection<StandardSchemaV1, infer Schema>
? MetaCollectionEntry<StandardSchemaV1.InferOutput<Schema>>[]
: never;
return out as never;
},
async docs<Name extends keyof Config & string>(
@@ -181,16 +183,8 @@ export function server<Config, TC extends InternalTypeConfig>(options: ServerOpt
base: string,
metaGlob: AwaitableGlobEntries<unknown>,
docGlob: AwaitableGlobEntries<unknown>,
) {
const entry = {
docs: await this.doc(name, base, docGlob),
meta: await this.meta(name, base, metaGlob),
toFumadocsSource(options) {
return toFumadocsSource(this.docs, this.meta, options);
},
} satisfies DocsCollectionEntry;
return entry as Config[Name] extends DocsCollection<infer Page, infer Meta>
): Promise<
Config[Name] extends DocsCollection<infer Page, infer Meta>
? StandardSchemaV1.InferOutput<Page> extends PageData
? StandardSchemaV1.InferOutput<Meta> extends MetaData
? DocsCollectionEntry<
@@ -201,7 +195,17 @@ export function server<Config, TC extends InternalTypeConfig>(options: ServerOpt
>
: never
: never
: never;
: never
> {
const entry = {
docs: await this.doc(name, base, docGlob),
meta: await this.meta(name, base, metaGlob),
toFumadocsSource(options) {
return toFumadocsSource(this.docs, this.meta, options);
},
} satisfies DocsCollectionEntry;
return entry as never;
},
async docsLazy<Name extends keyof Config & string>(
name: Name,
@@ -209,16 +213,8 @@ export function server<Config, TC extends InternalTypeConfig>(options: ServerOpt
metaGlob: AwaitableGlobEntries<unknown>,
docHeadGlob: AwaitableGlobEntries<unknown>,
docBodyGlob: Record<string, () => Promise<unknown>>,
) {
const entry = {
docs: await this.docLazy(name, base, docHeadGlob, docBodyGlob),
meta: await this.meta(name, base, metaGlob),
toFumadocsSource(options) {
return toFumadocsSource(this.docs, this.meta, options);
},
} satisfies AsyncDocsCollectionEntry;
return entry as Config[Name] extends DocsCollection<infer Page, infer Meta>
): Promise<
Config[Name] extends DocsCollection<infer Page, infer Meta>
? StandardSchemaV1.InferOutput<Page> extends PageData
? StandardSchemaV1.InferOutput<Meta> extends MetaData
? AsyncDocsCollectionEntry<
@@ -229,7 +225,17 @@ export function server<Config, TC extends InternalTypeConfig>(options: ServerOpt
>
: never
: never
: never;
: never
> {
const entry = {
docs: await this.docLazy(name, base, docHeadGlob, docBodyGlob),
meta: await this.meta(name, base, metaGlob),
toFumadocsSource(options) {
return toFumadocsSource(this.docs, this.meta, options);
},
} satisfies AsyncDocsCollectionEntry;
return entry as never;
},
};
}
+1 -1
View File
@@ -107,7 +107,7 @@
"@fastify/deepmerge": "3.2.1",
"@scalar/helpers": "0.8.0",
"@scalar/json-magic": "0.12.14",
"@scalar/openapi-upgrader": "0.2.7",
"@scalar/openapi-upgrader": "0.2.8",
"ajv": "8.20.0",
"fast-content-type-parse": "3.0.0",
"fast-deep-equal": "3.1.3",
+15 -21
View File
@@ -67,7 +67,7 @@ export interface OpenAPIServer {
) => Promise<Source<{ metaData: MetaData; pageData: OpenAPIPageData }>>;
/**
* Generate virtual pages for Fumadocs Source API
* Generate virtual pages for Fumadocs Source API (note: please disable cache to allow built-in revalidation)
*/
dynamicSource: (
options?: OpenAPISourceOptions,
@@ -88,6 +88,7 @@ export interface OpenAPIPageData extends PageData {
getClientAPIPageProps: () => Promise<ClientApiPageProps>;
structuredData: StructuredData;
toc: TOCItemType[];
_openapi?: InternalOpenAPIMeta;
}
export type OpenAPISourceOptions = SchemaToPagesOptions & {
@@ -117,15 +118,19 @@ export function createOpenAPI(options: OpenAPIOptions = {}): OpenAPIServer {
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 { fromSchema } = 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);
const schemas = await server.getSchemas();
const builderOptions = createAutoPreset(options);
for (const [id, schema] of Object.entries(schemas)) {
const list = fromSchema(id, schema, builderOptions);
onEntries(list);
function onEntry(entry: PageOutput | OperationOutput | WebhookOutput) {
const props = getPageProps(entry);
@@ -141,7 +146,7 @@ export function createOpenAPI(options: OpenAPIOptions = {}): OpenAPIServer {
async getClientAPIPageProps() {
return {
payload: {
bundled: processed.bundled,
bundled: schema.bundled,
proxyUrl: server.options.proxyUrl,
},
...props,
@@ -149,11 +154,11 @@ export function createOpenAPI(options: OpenAPIOptions = {}): OpenAPIServer {
},
getSchema() {
return {
id: schemaId,
...processed,
id,
...schema,
};
},
...toStaticData(props, processed.dereferenced),
...toStaticData(props, schema.dereferenced),
_openapi: {
method:
entry.type === 'operation' || entry.type === 'webhook'
@@ -211,8 +216,6 @@ export function createOpenAPI(options: OpenAPIOptions = {}): OpenAPIServer {
},
});
}
onEntries(list);
}
return files;
@@ -265,15 +268,6 @@ export function createCodeSample<T>(
return options;
}
declare module 'fumadocs-core/source' {
export interface PageData {
/**
* Added by Fumadocs OpenAPI
*/
_openapi?: InternalOpenAPIMeta;
}
}
export interface InternalOpenAPIMeta {
method?: string;
webhook?: boolean;
@@ -293,7 +287,7 @@ export function openapiPlugin(): LoaderPlugin {
const file = this.storage.read(filePath);
if (!file || file.format !== 'page') return node;
const openApiData = file.data._openapi;
const openApiData = (file.data as { _openapi?: InternalOpenAPIMeta })._openapi;
if (!openApiData || typeof openApiData !== 'object') return node;
if (openApiData.deprecated) {
@@ -98,25 +98,6 @@ interface ExtractedInfo {
})[];
}
export async function fromServer(
server: OpenAPIServer,
config: PagesBuilderConfig,
): Promise<Record<string, OutputEntry[]>> {
const schemas = await server.getSchemas();
const generated: Record<string, OutputEntry[]> = {};
const entries = Object.entries(schemas);
if (entries.length === 0) {
throw new Error('No input files found.');
}
for (const [id, schema] of entries) {
generated[id] = fromSchema(id, schema, config);
}
return generated;
}
export function fromSchema(
schemaId: string,
processed: DereferencedDocument,