Written by
Fuma Nama
At
Fri Sep 06 2024
Fumadocs MDX v10
相较于 Fumadocs MDX,我们内置的内容来源有了改进。
问题
🌐 The Problem
Fumadocs MDX 对文档非常有用。但我们也希望优先考虑灵活性和代码组织。
🌐 Fumadocs MDX worked great for docs. But we also want to prioritize flexibility and code organization.
以前,它是一个简单的 Webpack 加载器,用于将 MDX 转换为 JavaScript。
你将 MDX 处理器的选项传递给加载器,它会将其转换为 JavaScript 文件。
然后,将导出一个 .map.ts:
🌐 Previously, it was a simple Webpack loader that turns MDX into JavaScript.
You pass the MDX processor options to the loader, it turns them into JavaScript files.
Then, a .map.ts will be exported:
export const map = {
'docs/index.mdx': import('./docs/index.mdx'),
'docs/guide.mdx': import('./docs/guide.mdx'),
};你的 Next.js 应用将导入映射文件,并访问可用的 MDX 文件。
🌐 Your Next.js app will import the map file, and access the available MDX files.
这个模型可以工作,但我们开始看到一些问题:
🌐 This model works, but we started to see some problems:
-
没有内置方法来定义多个集合:
例如,我们有一个用于博客文章的目录和一个用于文档页面的目录。
在 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目录。如果我们想添加一个 仅对博客文章生效 的评论插件,在 Fumadocs MDX 中这是不可能的。一旦你应用了 remark 插件,它将在所有 MDX 文件中生效,包括 docs 目录中的 MDX 文件。
-
Turbopack 兼容性:
Turbopack 不允许将不可序列化的选项传递给加载器。然而,整个 MDX、remark 和 rehype 生态系统都使用函数作为插件。函数是不可序列化的,除非我们找到一个支持 Turbopack 的无缝解决方案,否则我们无法将 Fumadocs 迁移到 Turbopack。
-
编译时验证:
所有的模式验证无法在构建时完成,因为 MDX 加载器实际上 并不知道你在
source.ts中定义的集合。此外,Zod 模式是传递给
source.ts中的源适配器,而不是加载器:import { loader } from 'fumadocs-core/source'; import { createMDXSource } from 'fumadocs-mdx/runtime/next'; export const source = loader({ source: createMDXSource(map, { // schema }), });这会失去一些可能在打包器层面进行的性能优化。
-
未指定类型:
从
.map.ts文件导出的map对象类型为unknown,只有在与 Source APIloader一起使用时才有类型。这避免了自动生成类型的复杂性,但我想让它具备类型并且在没有 Source API 的情况下也能使用。
解决方案
🌐 The Solution
参考了一些内容集合和 Velite,我发现为 Fumadocs MDX 准备一个配置文件会非常棒。
🌐 Taking some references from Content Collections and Velite, I found it would be great to have a config file for Fumadocs MDX.
source.config.ts
我们可以让语法类似于内容集合和其他工具,以便简化采用过程。 要定义一个集合:
🌐 We can make the syntax similar to Content Collections and other tools, to make the adoption process easier. To define a collection:
import { z } from 'zod';
export const blog = defineCollections({
dir: './blog',
schema: z.object({
// the schema
}),
mdxOptions: {
// remark plugins?
},
});MDX 加载器读取配置文件,找到对应的文件集合,进行验证,并使用集合中的选项进行编译。
🌐 The MDX loader reads the config file, find the corresponding collection of the file, validate, and compile it using the options from collection.
这允许我们在不违反 Turbopack 规则的情况下正常传递 MDX 选项。
🌐 This allows us to pass MDX options normally without breaking Turbopack's rules.
实现
🌐 The Implementation
由于配置文件是用 TypeScript 编写的,我们需要一个打包工具来读取它。我使用了 esbuild,它是一个用 Go 编写的高性能打包工具。
🌐 As the config file is written in TypeScript, we will need a bundler to read it. I used esbuild, it is a performant bundler written in Go.
在打包配置文件后,动态导入将按预期工作。
🌐 After bundling the config file, a dynamic import will work as expected.
await import('./source.compiled.mjs');.map 文件
🌐 .map file
我们需要一个地方来导入已编译的集合。之前,我们只是通过 Webpack 插件生成一个 .map.ts 文件。它声明了类型,但没有实际数据。
🌐 We need a place to import the compiled collections.
Previously, we simply generate a .map.ts file with Webpack plugins.
It declares the types, with no actual data.
export declare const map: unknown;将使用加载器将 .map.ts 文件转换为上述输出:
🌐 A loader will be used to transform .map.ts file into the output aforementioned:
export const map = {
'docs/index.mdx': import('./docs/index.mdx'),
'docs/guide.mdx': import('./docs/guide.mdx'),
};生成的 .map.ts 永远不会改变,因为它不依赖于配置文件。无论你如何配置,它都会只导出一个 map 对象,其类型为 unknown。
🌐 The generated .map.ts never changes because it doesn't depend on the config file.
No matter how you configure it, there'll be only a map object exported, with a type of unknown.
现在,我们需要为每个集合生成类型,并且随着我们更改集合,类型可能会发生变化。之前的方法不再适用。
🌐 Now, we need to generate types for every collection, and the types may change as we change the collections. The previous approach is no longer applicable.
我将 .map.ts 文件重命名为 .source/index,index.d.ts 和 index.js 都是由 Fumadocs MDX 生成的,而不是使用加载器。
🌐 I renamed the .map.ts file to .source/index, both index.d.ts and index.js are generated by Fumadocs MDX, instead of using a loader.
已实现一个地图文件生成器,它读取配置文件并根据导出的集合生成输出。
🌐 A map file generator is implemented, it reads the config file and generate output based on exported collections.
自动重载
🌐 Auto-reload
我们想要关注变化:
🌐 We want to watch for changes:
- 当输入文件被添加或删除时,相应地在
.source/index.js文件中添加或删除相关条目。 - 当配置文件更改时,重新编译受影响的文件,并在
.source/index.d.ts更新生成的类型。
我选择了 chokidar 来监视文件变化,效果很好。文件监视器运行在 next.config.mjs 上,它与 MDX 加载器是独立的。
🌐 I chose chokidar to watch file changes, it worked well.
The file watcher lives on next.config.mjs, it's independent to the MDX loader.
为了在配置文件更改时通知打包工具,我们添加了一个哈希值。
🌐 To notify bundlers when config files changed, we added a hash.
export const collection1 = [import('./docs/index.mdx?hash=hashOfConfigFile')];随着配置哈希的改变,文件将被重新编译。
🌐 The file will be re-compiled as the config hash changes.
为了优化性能,我们还添加了集合名称。
🌐 To optimize performance, we also added the collection name.
export const collection1 = [
import('./docs/index.mdx?hash=hashOfConfigFile&collection=collection1'),
];加载器从资源查询中获取输入文件的集合,而无需额外步骤来检测其相关的集合。
🌐 The loader obtains the collection of input file from resource query, without taking extra steps to detect its associated collection.
结果
🌐 Result
将生成一个 .source/index 文件,它是完全类型化的。随着你修改配置文件,文件将被重新编译。
🌐 A .source/index file will be generated, it is fully typed.
The files will be re-compiled as you modify the config file.
- 支持 Turbopack,我们在仓库中有一个使用 Turbopack 的小示例。
- 多个集合,每个都有自己的 MDX 选项。
- 运行时 + 构建时验证与转换。
问题
🌐 Questions
我认为还有改进的空间:
🌐 I think there is still room for improvement:
- 使用 Turbopack 原生方式来打包配置文件吗?
- 懒加载打包/导入 MDX 文件的主体内容吗?
请给我关于 Fumadocs MDX 重新设计的反馈 ;)
🌐 Please give me feedback about the redesign of Fumadocs MDX ;)