问题
Fumadocs MDX 在文档方面工作良好。但我们也希望优先考虑灵活性和代码组织。
以前,它是一个简单的 Webpack loader,将 MDX 转换为 JavaScript。
您将 MDX 处理器选项传递给 loader,它会将它们转换为 JavaScript 文件。
然后,会导出一个 .map.ts:
export const map = {
'docs/index.mdx': import('./docs/index.mdx'),
'docs/guide.mdx': import('./docs/guide.mdx'),
};您的 Next.js 应用会导入 map 文件,并访问可用的 MDX 文件。
这种模型有效,但我们开始看到一些问题:
-
没有内置方式定义多个集合:
例如,我们有一个博客文章目录和一个文档页面目录。
在 Fumadocs MDX 中,所有这些资源都被转换为
.map.ts导出的单个对象:export const map = { 'blog/post.mdx': import('./blog/post.mdx'), 'docs/index.mdx': import('./docs/index.mdx'), };我们使用 Source API 的
rootDir选项实现了一个解决方案,但这不是理想的方式。 这也给我们带来了另一个问题: -
每个集合的不同 MDX 选项:
与上面的例子相同,我们有一个
/blog目录用于博客文章。 如果我们想添加一个 仅适用于博客文章 的 remark 插件,使用 Fumadocs MDX 这是不可能的。一旦应用 remark 插件,它就会对所有 MDX 文件生效,包括文档目录中的 MDX 文件。
-
Turbopack 兼容性:
Turbopack 不允许将非可序列化选项传递给 loader。 然而,整个 MDX、remark 和 rehype 生态系统都使用函数作为插件。 函数不可序列化,除非找到一个无缝支持 Turbopack 的解决方案,否则我们无法将 Fumadocs 迁移到 Turbopack。
-
编译时验证:
所有模式验证都无法在构建时进行,因为 MDX loader 实际上 不知道您在
source.ts中定义的集合。此外,Zod 模式被传递到
source.ts中的 source adapter,而不是 loader:import { loader } from 'fumadocs-core/source'; import { createMDXSource } from 'fumadocs-mdx'; export const source = loader({ source: createMDXSource(map, { // schema }), });这会丢失一些可以在打包器级别进行的性能优化。
-
无类型:
从
.map.ts文件导出的map对象类型为unknown,只有在使用 Source API 的loader时才会被类型化。这避免了自动生成类型的复杂性,但我希望让它具有类型化,并且无需 Source API 即可使用。
解决方案
参考 Content Collections 和 Velite,我发现为 Fumadocs MDX 有一个配置文件会很棒。
source.config.ts
我们可以使语法类似于 Content Collections 和其他工具,以使采用过程更容易。 要定义一个集合:
import { z } from 'zod';
export const blog = defineCollections({
dir: './blog',
schema: z.object({
// the schema
}),
mdxOptions: {
// remark plugins?
},
});MDX loader 读取配置文件,找到文件的对应集合,进行验证,并使用集合中的选项编译它。
这允许我们正常传递 MDX 选项,而不会破坏 Turbopack 的规则。
实现
由于配置文件是用 TypeScript 编写的,我们需要一个打包器来读取它。 我使用了 esbuild,它是一个用 Go 编写的性能出色的打包器。
在打包配置文件后,动态导入将按预期工作。
await import('./source.compiled.mjs');.map 文件
我们需要一个地方来导入编译后的集合。
以前,我们简单地使用 Webpack 插件生成一个 .map.ts 文件。
它声明了类型,但没有实际数据。
export declare const map: unknown;一个 loader 将用于将 .map.ts 文件转换为前面提到的输出:
export const map = {
'docs/index.mdx': import('./docs/index.mdx'),
'docs/guide.mdx': import('./docs/guide.mdx'),
};生成的 .map.ts 永不变更,因为它不依赖配置文件。
无论您如何配置,都只会导出一个类型为 unknown 的 map 对象。
现在,我们需要为每个集合生成类型,并且类型可能会随着我们更改集合而变化。 以前的方法不再适用。
我将 .map.ts 文件重命名为 .source/index,index.d.ts 和 index.js 都由 Fumadocs MDX 生成,而不是使用 loader。
实现了一个 map 文件生成器,它读取配置文件并基于导出的集合生成输出。
自动重新加载
我们希望监视更改:
- 当输入文件添加/删除时,在
.source/index.js文件中添加或删除相关条目。 - 当配置文件更改时,重新编译受影响的文件,并更新
.source/index.d.ts中的生成类型。
我选择了 chokidar 来监视文件更改,它工作良好。
文件监视器位于 next.config.mjs 中,它独立于 MDX loader。
为了在配置文件更改时通知打包器,我们添加了一个 hash。
export const collection1 = [import('./docs/index.mdx?hash=hashOfConfigFile')];当配置 hash 更改时,文件将被重新编译。
为了优化性能,我们还添加了集合名称。
export const collection1 = [
import('./docs/index.mdx?hash=hashOfConfigFile&collection=collection1'),
];loader 从 resource query 获取输入文件的集合,而无需额外步骤来检测其关联集合。
结果
将生成一个 .source/index 文件,它完全类型化。
当您修改配置文件时,文件将被重新编译。
- 支持 Turbopack,我们在仓库中有一个使用 Turbopack 的小示例。
- 多个集合,每个集合都有自己的 MDX 选项。
- 运行时 + 构建时验证和转换。
问题
我认为还有改进的空间:
- 使用 Turbopack 原生功能来打包配置文件?
- 懒加载/导入 MDX 文件的主要内容?
请给我反馈关于 Fumadocs MDX 重设计的意见 ;)
Written by
Fuma Nama
At
Fri Sep 06 2024