Core: Support MDX preset

This commit is contained in:
Fuma Nama
2025-12-13 18:00:55 +08:00
parent 18c7e7ecea
commit 7292424305
5 changed files with 217 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': patch
---
Support MDX preset in Fumadocs Core
@@ -0,0 +1,95 @@
import type { ProcessorOptions } from '@mdx-js/mdx';
import type * as Plugins from '@/mdx-plugins';
import { resolvePlugins, type ResolvePlugins } from '@/content/mdx/util';
export type MDXBundlerPresetOptions = Omit<
NonNullable<ProcessorOptions>,
'rehypePlugins' | 'remarkPlugins'
> & {
rehypePlugins?: ResolvePlugins;
remarkPlugins?: ResolvePlugins;
remarkStructureOptions?: Plugins.StructureOptions | false;
remarkHeadingOptions?: Plugins.RemarkHeadingOptions;
remarkImageOptions?: Plugins.RemarkImageOptions | false;
remarkCodeTabOptions?: Plugins.RemarkCodeTabOptions | false;
remarkNpmOptions?: Plugins.RemarkNpmOptions | false;
rehypeCodeOptions?: Plugins.RehypeCodeOptions | false;
};
/**
* apply MDX processor presets
*/
export async function mdxPreset(
options: MDXBundlerPresetOptions = {},
): Promise<ProcessorOptions> {
const {
rehypeCodeOptions,
remarkImageOptions,
remarkHeadingOptions,
remarkStructureOptions,
remarkCodeTabOptions,
remarkNpmOptions,
...mdxOptions
} = options;
const remarkPlugins = await resolvePlugins(
(v) => [
import('remark-gfm').then((mod) => mod.default),
import('@/mdx-plugins/remark-heading').then((mod) => [
mod.remarkHeading,
{
generateToc: false,
...remarkHeadingOptions,
},
]),
remarkImageOptions !== false &&
import('@/mdx-plugins/remark-image').then((mod) => [
mod.remarkImage,
{
...remarkImageOptions,
useImport: remarkImageOptions?.useImport ?? true,
},
]),
remarkCodeTabOptions !== false &&
import('@/mdx-plugins/remark-code-tab').then((mod) => [
mod.remarkCodeTab,
remarkCodeTabOptions,
]),
remarkNpmOptions !== false &&
import('@/mdx-plugins/remark-npm').then((mod) => [
mod.remarkNpm,
remarkNpmOptions,
]),
...v,
remarkStructureOptions !== false &&
import('@/mdx-plugins/remark-structure').then((mod) => [
mod.remarkStructure,
{
exportAs: 'structuredData',
...remarkStructureOptions,
} satisfies Plugins.StructureOptions,
]),
],
mdxOptions.remarkPlugins,
);
const rehypePlugins = await resolvePlugins(
(v) => [
rehypeCodeOptions !== false &&
import('@/mdx-plugins/rehype-code').then((mod) => [
mod.rehypeCode,
rehypeCodeOptions,
]),
...v,
import('@/mdx-plugins/rehype-toc').then((mod) => mod.rehypeToc),
],
mdxOptions.rehypePlugins,
);
return {
...mdxOptions,
remarkPlugins,
rehypePlugins,
};
}
@@ -0,0 +1,94 @@
import type { ProcessorOptions } from '@mdx-js/mdx';
import type * as Plugins from '@/mdx-plugins';
import { resolvePlugins, type ResolvePlugins } from '@/content/mdx/util';
export type MDXRuntimePresetOptions = Omit<
NonNullable<ProcessorOptions>,
'rehypePlugins' | 'remarkPlugins'
> & {
rehypePlugins?: ResolvePlugins;
remarkPlugins?: ResolvePlugins;
remarkStructureOptions?: Plugins.StructureOptions | false;
remarkHeadingOptions?: Plugins.RemarkHeadingOptions;
remarkImageOptions?: Plugins.RemarkImageOptions | false;
remarkCodeTabOptions?: Plugins.RemarkCodeTabOptions | false;
remarkNpmOptions?: Plugins.RemarkNpmOptions | false;
rehypeCodeOptions?: Plugins.RehypeCodeOptions | false;
};
/**
* apply MDX processor presets
*/
export async function mdxPreset(
options: MDXRuntimePresetOptions = {},
): Promise<ProcessorOptions> {
const {
rehypeCodeOptions,
remarkImageOptions,
remarkHeadingOptions,
remarkStructureOptions,
remarkCodeTabOptions,
remarkNpmOptions,
outputFormat = 'function-body',
...mdxOptions
} = options;
const remarkPlugins = await resolvePlugins(
(v) => [
import('remark-gfm').then((mod) => mod.default),
import('@/mdx-plugins/remark-heading').then((mod) => [
mod.remarkHeading,
{
generateToc: false,
...remarkHeadingOptions,
},
]),
remarkImageOptions !== false &&
import('@/mdx-plugins/remark-image').then((mod) => [
mod.remarkImage,
remarkImageOptions,
]),
remarkCodeTabOptions !== false &&
import('@/mdx-plugins/remark-code-tab').then((mod) => [
mod.remarkCodeTab,
remarkCodeTabOptions,
]),
remarkNpmOptions !== false &&
import('@/mdx-plugins/remark-npm').then((mod) => [
mod.remarkNpm,
remarkNpmOptions,
]),
...v,
remarkStructureOptions !== false &&
import('@/mdx-plugins/remark-structure').then((mod) => [
mod.remarkStructure,
{
exportAs: 'structuredData',
...remarkStructureOptions,
} satisfies Plugins.StructureOptions,
]),
],
mdxOptions.remarkPlugins,
);
const rehypePlugins = await resolvePlugins(
(v) => [
rehypeCodeOptions !== false &&
import('@/mdx-plugins/rehype-code').then((mod) => [
mod.rehypeCode,
rehypeCodeOptions,
]),
...v,
import('@/mdx-plugins/rehype-toc').then((mod) => mod.rehypeToc),
],
mdxOptions.rehypePlugins,
);
return {
...mdxOptions,
outputFormat,
remarkPlugins,
rehypePlugins,
};
}
+22
View File
@@ -0,0 +1,22 @@
import type { Pluggable } from 'unified';
type Thenable<T> = T | PromiseLike<T>;
export type ResolvePlugins =
| Thenable<Pluggable>[]
| ((v: Pluggable[]) => Thenable<Pluggable>[]);
export async function resolvePlugins(
def: (v: Thenable<Pluggable>[]) => Thenable<Pluggable | false>[],
options: ResolvePlugins = [],
): Promise<Pluggable[]> {
const list = (
await Promise.all(def(Array.isArray(options) ? options : []))
).filter((v) => v !== false);
if (typeof options === 'function') {
return Promise.all(options(list));
}
return list;
}
+1
View File
@@ -8,6 +8,7 @@ export default defineConfig({
'src/{toc,link,breadcrumb,dynamic-link}.tsx',
'src/page-tree/index.ts',
'src/{negotiation,content}/*',
'src/content/mdx/preset-*.ts',
'src/source/{index,schema}.ts',
'src/source/client/*.{ts,tsx}',
'src/source/plugins/lucide-icons.ts',