Core: default the type of plugins to LoaderPluginOption[]

This commit is contained in:
Fuma Nama
2025-11-20 22:27:45 +08:00
parent 4f1c0c54fc
commit 15bd183062
4 changed files with 85 additions and 31 deletions
+25
View File
@@ -0,0 +1,25 @@
---
'fumadocs-core': minor
---
**[Loader API] Default the type of `plugins` to `LoaderPluginOption[]`**
It should no longer enforce type checks on custom properties from your content source.
For creating fully typed plugins (with custom properties), use the following pattern:
```ts
import { loader } from 'fumadocs-core/source';
import { docs } from 'fumadocs-mdx:collections/server';
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
export const source = loader(docs.toFumadocsSource(), {
baseUrl: '/docs',
plugins: ({ typedPlugin }) => [
lucideIconsPlugin(),
typedPlugin({
// the plugin config
}),
],
});
```
+31 -17
View File
@@ -20,7 +20,11 @@ import {
} from '@/source/page-tree/builder';
import { joinPath } from './path';
import { normalizeUrl } from '@/utils/normalize-url';
import { buildPlugins, type LoaderPlugin } from '@/source/plugins';
import {
buildPlugins,
type LoaderPluginOption,
type LoaderPlugin,
} from '@/source/plugins';
import { slugsPlugin } from '@/source/plugins/slugs';
import { iconPlugin, type IconResolver } from '@/source/plugins/icon';
@@ -29,26 +33,21 @@ export interface LoaderConfig {
i18n: I18nConfig | undefined;
}
export interface SourceConfig {
pageData: PageData;
metaData: MetaData;
}
export interface LoaderOptions<
S extends SourceConfig = SourceConfig,
I18n extends I18nConfig | undefined = I18nConfig | undefined,
> {
export interface LoaderOptions<C extends LoaderConfig = LoaderConfig> {
baseUrl: string;
i18n?: I18n;
i18n?: C['i18n'];
url?: UrlFn;
/**
* Additional options for page tree builder
*/
pageTree?: PageTreeOptions<S>;
plugins?: (LoaderPlugin<S> | LoaderPlugin<S>[] | undefined)[];
pageTree?: PageTreeOptions<C>;
plugins?:
| LoaderPluginOption[]
| ((context: {
typedPlugin: (plugin: LoaderPlugin<C>) => LoaderPlugin;
}) => LoaderPluginOption[]);
icon?: IconResolver;
slugs?: (info: { path: string }) => string[];
}
@@ -66,6 +65,11 @@ export interface Source<Config extends SourceConfig = SourceConfig> {
files: VirtualFile<Config>[];
}
export interface SourceConfig {
pageData: PageData;
metaData: MetaData;
}
interface SharedFileInfo {
/**
* Virtualized file path (relative to content directory)
@@ -227,7 +231,10 @@ export function loader<
I18n extends I18nConfig | undefined = undefined,
>(
source: Source<Config>,
options: LoaderOptions<NoInfer<Config>, I18n>,
options: LoaderOptions<{
source: NoInfer<Config>;
i18n: I18n;
}>,
): LoaderOutput<{
source: Config;
i18n: I18n;
@@ -237,7 +244,10 @@ export function loader<
Config extends SourceConfig,
I18n extends I18nConfig | undefined = undefined,
>(
options: LoaderOptions<NoInfer<Config>, I18n> & {
options: LoaderOptions<{
source: NoInfer<Config>;
i18n: I18n;
}> & {
source: Source<Config>;
},
): LoaderOutput<{
@@ -277,7 +287,11 @@ function resolveConfig(
plugins: buildPlugins([
slugsPlugin(slugs),
icon && iconPlugin(icon),
...plugins,
...(typeof plugins === 'function'
? plugins({
typedPlugin: (plugin) => plugin as unknown as LoaderPlugin,
})
: plugins),
]),
};
@@ -1,5 +1,5 @@
import type * as PageTree from '@/page-tree/definitions';
import type { MetaData, PageData, SourceConfig, UrlFn } from '@/source';
import type { LoaderConfig, MetaData, PageData, UrlFn } from '@/source';
import type { ContentStorage } from '@/source/storage/content';
import { basename, extname, joinPath } from '@/source/path';
import { transformerFallback } from '@/source/page-tree/transformer-fallback';
@@ -47,7 +47,7 @@ export interface PageTreeTransformer<
) => PageTree.Root;
}
export interface PageTreeOptions<Config extends SourceConfig = SourceConfig> {
export interface PageTreeOptions<Config extends LoaderConfig = LoaderConfig> {
id?: string;
/**
* Remove references to the file path of original nodes (`$ref`)
@@ -65,7 +65,10 @@ export interface PageTreeOptions<Config extends SourceConfig = SourceConfig> {
/**
* Additional page tree transformers to apply
*/
transformers?: PageTreeTransformer<Config['pageData'], Config['metaData']>[];
transformers?: PageTreeTransformer<
Config['source']['pageData'],
Config['source']['metaData']
>[];
}
export interface PageTreeBuilder {
+23 -11
View File
@@ -1,8 +1,8 @@
import type { ContentStorage } from '@/source/storage/content';
import type { PageTreeTransformer } from '@/source/page-tree/builder';
import type { ResolvedLoaderConfig, SourceConfig } from '@/source/loader';
import type { LoaderConfig, ResolvedLoaderConfig } from '@/source/loader';
export interface LoaderPlugin<Config extends SourceConfig = SourceConfig> {
export interface LoaderPlugin<Config extends LoaderConfig = LoaderConfig> {
name?: string;
/**
@@ -23,18 +23,26 @@ export interface LoaderPlugin<Config extends SourceConfig = SourceConfig> {
* transform the storage after loading
*/
transformStorage?: (context: {
storage: ContentStorage<Config['pageData'], Config['metaData']>;
storage: ContentStorage<
Config['source']['pageData'],
Config['source']['metaData']
>;
}) => void;
/**
* transform the generated page tree
*/
transformPageTree?: PageTreeTransformer<
Config['pageData'],
Config['metaData']
Config['source']['pageData'],
Config['source']['metaData']
>;
}
export type LoaderPluginOption<Config extends LoaderConfig = LoaderConfig> =
| LoaderPlugin<Config>
| LoaderPluginOption<Config>[]
| undefined;
const priorityMap = {
pre: 1,
default: 0,
@@ -42,17 +50,21 @@ const priorityMap = {
};
export function buildPlugins(
plugins: (LoaderPlugin | LoaderPlugin[] | undefined)[],
plugins: LoaderPluginOption[],
sort = true,
): LoaderPlugin[] {
const flatten: LoaderPlugin[] = [];
for (const plugin of plugins) {
if (Array.isArray(plugin)) flatten.push(...plugin);
if (Array.isArray(plugin)) flatten.push(...buildPlugins(plugin, false));
else if (plugin) flatten.push(plugin);
}
return flatten.sort(
(a, b) =>
priorityMap[b.enforce ?? 'default'] - priorityMap[a.enforce ?? 'default'],
);
if (sort)
return flatten.sort(
(a, b) =>
priorityMap[b.enforce ?? 'default'] -
priorityMap[a.enforce ?? 'default'],
);
return flatten;
}