Core: Support name & config options in Loader Plugin

This commit is contained in:
Fuma Nama
2025-10-04 23:45:13 +08:00
parent 2cc3246430
commit ce2be59c5c
7 changed files with 171 additions and 96 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': patch
---
Loader Plugin: support `name` & `config` options
+66 -41
View File
@@ -20,7 +20,7 @@ import {
parseFilePath,
} from './path';
import { normalizeUrl } from '@/utils/normalize-url';
import { buildPlugins, type LoaderPlugins } from '@/source/plugins';
import { buildPlugins, type LoaderPlugin } from '@/source/plugins';
import { slugsPlugin } from '@/source/plugins/slugs';
import {
compatPlugin,
@@ -39,29 +39,42 @@ export interface SourceConfig {
metaData: MetaData;
}
export type LoaderOptions<
Config extends SourceConfig = SourceConfig,
export interface LoaderOptions<
S extends SourceConfig = SourceConfig,
I18n extends I18nConfig | undefined = I18nConfig | undefined,
> = BaseLoaderOptions<NoInfer<Config>> & {
source: Source<Config> | Source<Config>[];
/**
* Configure i18n
*/
i18n?: I18n;
};
interface BaseLoaderOptions<Config extends SourceConfig>
extends LegacyLoaderOptions {
baseUrl: string;
icon?: IconResolver;
slugs?: (info: FileInfo) => string[];
url?: UrlFn;
> extends Omit<ResolvedLoaderConfig<S, I18n>, 'plugins' | 'pageTree'>,
LegacyLoaderOptions {
/**
* Additional options for page tree builder
*/
pageTree?: Partial<BaseOptions> &
LegacyPageTreeOptions<Config['pageData'], Config['metaData']>;
plugins?: LoaderPlugins<Config['pageData'], Config['metaData']>;
LegacyPageTreeOptions<NoInfer<S>['pageData'], NoInfer<S>['metaData']>;
plugins?: (
| LoaderPlugin<NoInfer<S>['pageData'], NoInfer<S>['metaData']>
| undefined
)[];
icon?: IconResolver;
slugs?: (info: FileInfo) => string[];
}
export interface ResolvedLoaderConfig<
S extends SourceConfig = SourceConfig,
I18n extends I18nConfig | undefined = I18nConfig | undefined,
> {
source: Source<S> | Source<S>[];
/**
* Configure i18n
*/
i18n?: I18n;
baseUrl: string;
url?: UrlFn;
pageTree?: Partial<BaseOptions>;
plugins?: LoaderPlugin<NoInfer<S>['pageData'], NoInfer<S>['metaData']>[];
}
export interface Source<Config extends SourceConfig> {
@@ -269,7 +282,7 @@ export function loader<
i18n: I18n;
}> {
// @ts-expect-error -- forced type cast
return createOutput(options);
return createOutput(resolveConfig(options));
}
function loadSource<T extends SourceConfig>(source: Source<T> | Source<T>[]) {
@@ -286,27 +299,39 @@ function loadSource<T extends SourceConfig>(source: Source<T> | Source<T>[]) {
return out;
}
function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
if (!options.url && !options.baseUrl) {
console.warn('`loader()` now requires a `baseUrl` option to be defined.');
function resolveConfig({
slugs,
icon,
plugins = [],
...base
}: LoaderOptions): ResolvedLoaderConfig {
let config: ResolvedLoaderConfig = {
...base,
plugins: buildPlugins([
slugsPlugin(slugs),
icon && iconPlugin(icon),
compatPlugin(base, base.pageTree),
...plugins,
]),
};
for (const plugin of config.plugins ?? []) {
const result = plugin.config?.(config);
if (result) config = result;
}
const { source, baseUrl = '/', i18n, slugs: slugsFn, url: urlFn } = options;
return config;
}
function createOutput(
config: ResolvedLoaderConfig,
): LoaderOutput<LoaderConfig> {
const { source, baseUrl = '/', url: urlFn, i18n } = config;
const getUrl: UrlFn = urlFn
? (...args) => normalizeUrl(urlFn(...args))
: createGetUrl(baseUrl, i18n);
const defaultLanguage = i18n?.defaultLanguage ?? '';
const files = loadSource(source);
const plugins = [slugsPlugin(slugsFn)];
if (options.icon) {
plugins.push(iconPlugin(options.icon));
}
if (options.plugins) {
plugins.push(...buildPlugins(options.plugins));
}
if (options.pageTree) {
plugins.push(...compatPlugin(options, options.pageTree));
}
const storages = loadFiles(
files,
@@ -329,7 +354,7 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
data: file.data,
} as MetaFile;
},
plugins,
plugins: config.plugins,
},
i18n ?? {
defaultLanguage,
@@ -347,8 +372,8 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
get pageTree() {
pageTree ??= builder.buildI18n({
storages,
plugins,
...options.pageTree,
plugins: config.plugins,
...config.pageTree,
});
return i18n
@@ -398,9 +423,9 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
},
getLanguages() {
const list: LanguageEntry[] = [];
if (!options.i18n) return list;
if (!i18n) return list;
for (const language of options.i18n.languages) {
for (const language of i18n.languages) {
list.push({
language,
pages: this.getPages(language),
@@ -425,7 +450,7 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
return walker.pathToPage.get(`${language}.${ref}`);
},
getPageTree(locale) {
if (options.i18n) {
if (i18n) {
return this.pageTree[
(locale ?? defaultLanguage) as keyof typeof pageTree
];
@@ -435,7 +460,7 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
},
// @ts-expect-error -- ignore this
generateParams(slug, lang) {
if (options.i18n) {
if (i18n) {
return this.getLanguages().flatMap((entry) =>
entry.pages.map((page) => ({
[slug ?? 'slug']: page.slugs,
@@ -29,8 +29,6 @@ export interface PageTreeTransformer<
Page extends PageData = PageData,
Meta extends MetaData = MetaData,
> {
name?: string;
file?: (
this: PageTreeBuilderContext<Page, Meta>,
node: PageTree.Item,
+60 -45
View File
@@ -50,56 +50,71 @@ export interface LegacyPageTreeOptions<
*/
export function compatPlugin<Page extends PageData, Meta extends MetaData>(
loader: LegacyLoaderOptions,
{
attachFile,
attachSeparator,
attachFolder,
transformers,
}: LegacyPageTreeOptions<Page, Meta>,
) {
const chunk: LoaderPlugin<Page, Meta>[] = [];
chunk.push({
transformPageTree: {
file(node, file) {
if (!attachFile) return node;
const content = file ? this.storage.read(file) : undefined;
pageTreeOptions?: LegacyPageTreeOptions<Page, Meta>,
): LoaderPlugin<Page, Meta> {
return {
name: 'fumadocs:compat',
config(config) {
const plugins = (
config.plugins ? [...config.plugins] : []
) as LoaderPlugin<Page, Meta>[];
return attachFile(
node,
content?.format === 'page' ? content : undefined,
if (pageTreeOptions) {
const { attachFile, attachSeparator, attachFolder, transformers } =
pageTreeOptions;
for (const transformer of transformers ?? []) {
plugins.push(fromPageTreeTransformer(transformer));
}
plugins.push(
fromPageTreeTransformer({
file(node, file) {
if (!attachFile) return node;
const content = file ? this.storage.read(file) : undefined;
return attachFile(
node,
content?.format === 'page' ? content : undefined,
);
},
folder(node, folderPath, metaPath) {
if (!attachFolder) return node;
const files = this.storage.readDir(folderPath) ?? [];
const meta = metaPath ? this.storage.read(metaPath) : undefined;
return attachFolder(
node,
{
children: files.flatMap(
(file) => this.storage.read(file) ?? [],
),
},
meta?.format === 'meta' ? meta : undefined,
);
},
separator(node) {
if (!attachSeparator) return node;
return attachSeparator(node);
},
}),
);
},
folder(node, folderPath, metaPath) {
if (!attachFolder) return node;
}
const files = this.storage.readDir(folderPath) ?? [];
const meta = metaPath ? this.storage.read(metaPath) : undefined;
if (loader.transformers) {
for (const transformer of loader.transformers) {
plugins.push(fromStorageTransformer(transformer));
}
}
return attachFolder(
node,
{
children: files.flatMap((file) => this.storage.read(file) ?? []),
},
meta?.format === 'meta' ? meta : undefined,
);
},
separator(node) {
if (!attachSeparator) return node;
return attachSeparator(node);
},
return {
...config,
plugins: plugins as LoaderPlugin[],
};
},
});
for (const transformer of loader.transformers ?? []) {
chunk.push(fromStorageTransformer(transformer));
}
for (const transformer of transformers ?? []) {
chunk.push(fromPageTreeTransformer(transformer));
}
return chunk;
};
}
function fromPageTreeTransformer<Page extends PageData, Meta extends MetaData>(
+1
View File
@@ -13,6 +13,7 @@ export function iconPlugin(resolveIcon: IconResolver): LoaderPlugin {
}
return {
name: 'fumadocs:icon',
transformPageTree: {
file: replaceIcon,
folder: replaceIcon,
+37 -7
View File
@@ -1,16 +1,26 @@
import type { ContentStorage } from '@/source/load-files';
import type { PageTreeTransformer } from '@/source/page-tree/builder';
import type { MetaData, PageData } from '@/source/types';
export type LoaderPlugins<
Page extends PageData,
Meta extends MetaData,
> = LoaderPlugin<Page, Meta>[];
import type { ResolvedLoaderConfig } from '@/source/loader';
export interface LoaderPlugin<
Page extends PageData = PageData,
Meta extends MetaData = MetaData,
> {
name?: string;
/**
* Change the order of plugin:
* - `pre`: before normal plugins
* - `post`: after normal plugins
*/
enforce?: 'pre' | 'post';
/**
* receive & replace loader options
*/
config?: (config: ResolvedLoaderConfig) => ResolvedLoaderConfig | undefined;
/**
* transform the storage after loading
*/
@@ -23,7 +33,27 @@ export interface LoaderPlugin<
}
export function buildPlugins<Page extends PageData, Meta extends MetaData>(
plugins: LoaderPlugins<Page, Meta>,
plugins: (LoaderPlugin<Page, Meta> | undefined)[],
): LoaderPlugin<Page, Meta>[] {
return plugins;
const out: LoaderPlugin<Page, Meta>[] = [];
for (const plugin of plugins) {
if (plugin && plugin.enforce === 'pre') {
out.push(plugin);
}
}
for (const plugin of plugins) {
if (plugin && plugin.enforce === undefined) {
out.push(plugin);
}
}
for (const plugin of plugins) {
if (plugin && plugin.enforce === 'post') {
out.push(plugin);
}
}
return out;
}
+2 -1
View File
@@ -1,5 +1,5 @@
import { type FileInfo, getSlugs, parseFilePath } from '@/source';
import type { LoaderPlugin } from '@/source/plugins/index';
import type { LoaderPlugin } from '@/source/plugins';
import { basename, extname } from '@/source/path';
/**
@@ -13,6 +13,7 @@ export function slugsPlugin(
}
return {
name: 'fumadocs:slugs',
transformStorage({ storage }) {
const indexFiles = new Set<string>();
const taken = new Set<string>();