MDX: Support Rolldown integration

This commit is contained in:
Fuma Nama
2026-05-12 18:47:27 +08:00
parent c5b2ed4109
commit 819b6ec2d2
10 changed files with 157 additions and 70 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-mdx': patch
---
Support Rolldown integration
+5
View File
@@ -29,6 +29,7 @@
"./plugins/index-file": "./dist/plugins/index-file.js",
"./plugins/json-schema": "./dist/plugins/json-schema.js",
"./plugins/last-modified": "./dist/plugins/last-modified.js",
"./rolldown": "./dist/rolldown/index.js",
"./runtime/browser": "./dist/runtime/browser.js",
"./runtime/dynamic": "./dist/runtime/dynamic.js",
"./runtime/server": "./dist/runtime/server.js",
@@ -96,9 +97,13 @@
"mdast-util-directive": "*",
"next": "^15.3.0 || ^16.0.0",
"react": "^19.2.0",
"rolldown": "*",
"vite": "7.x.x || 8.x.x"
},
"peerDependenciesMeta": {
"rolldown": {
"optional": true
},
"mdast-util-directive": {
"optional": true
},
+31 -24
View File
@@ -2,7 +2,7 @@ import type { CompilerOptions } from '@/loaders/mdx/build-mdx';
import type { LoadFnOutput, LoadHook } from 'node:module';
import { fileURLToPath } from 'node:url';
import fs from 'node:fs/promises';
import type { TransformPluginContext } from 'rolldown';
import type { HookFilter, TransformPluginContext } from 'rolldown';
import type { Environment, TransformResult } from 'vite';
import { parse } from 'node:querystring';
import { ValidationError } from '@/utils/validation';
@@ -90,10 +90,10 @@ export function toNode(loader: Loader): LoadHook {
}
export interface ViteLoader {
filter: (id: string) => boolean;
filter: HookFilter;
transform: (
this: TransformPluginContext & { environment: Environment },
this: TransformPluginContext & { environment?: Environment },
value: string,
id: string,
) => Promise<TransformResult | null>;
@@ -101,33 +101,40 @@ export interface ViteLoader {
export function toVite(loader: Loader): ViteLoader {
return {
filter(id) {
return !loader.test || loader.test.test(id);
filter: {
id: loader.test,
},
async transform(value, id) {
const environment = this.environment;
const [file, query = ''] = id.split('?', 2);
const result = await loader.load({
filePath: file,
query: parse(query),
getSource() {
return value;
},
development: environment.mode === 'dev',
compiler: {
addDependency: (file) => {
this.addWatchFile(file);
try {
const result = await loader.load({
filePath: file,
query: parse(query),
getSource() {
return value;
},
},
});
development: this.environment ? this.environment.mode === 'dev' : false,
compiler: {
addDependency: (file) => {
this.addWatchFile(file);
},
},
});
if (result === null) return null;
return {
code: result.code,
map: result.map as TransformResult['map'],
moduleType: result.moduleType,
};
if (result === null) return null;
return {
code: result.code,
map: result.map as TransformResult['map'],
moduleType: result.moduleType,
};
} catch (e) {
if (e instanceof ValidationError) {
throw new Error(await e.toStringFormatted(), { cause: e });
}
throw e;
}
},
};
}
+3 -2
View File
@@ -1,5 +1,5 @@
import { frontmatter } from 'fumadocs-core/content/md/frontmatter';
import type { Loader } from '@/loaders/adapter';
import type { Loader, LoaderOutput } from '@/loaders/adapter';
import { z } from 'zod';
import type { DocCollectionItem } from '@/config/build';
import fs from 'node:fs/promises';
@@ -96,8 +96,9 @@ export function createMdxLoader({ getCore }: ConfigLoader): Loader {
environment: 'bundler',
});
const out = {
const out: LoaderOutput = {
code: String(compiled.value),
moduleType: 'js',
map: compiled.map,
};
@@ -271,7 +271,10 @@ export function remarkInclude(this: Processor): Transformer<Root, Root> {
const attributes = parseElementAttributes(node);
const { file: relativePath, section } = parseSpecifier(specifier);
const targetPath = path.resolve('cwd' in attributes ? file.cwd : file.dirname!, relativePath);
const targetPath = path.resolve(
'cwd' in attributes || !file.dirname ? file.cwd : file.dirname,
relativePath,
);
queue.push(
embedContent(targetPath, section, attributes, file).then((replace) => {
+1 -1
View File
@@ -181,7 +181,7 @@ async function init(dev: boolean, core: Core): Promise<void> {
}
}
export async function postInstall(options: CreateMDXOptions) {
export async function postInstall(options: CreateMDXOptions = {}) {
const core = createNextCore(applyDefaults(options));
await core.init({
config: loadConfig(core, true),
+1 -1
View File
@@ -9,7 +9,7 @@ export interface NodeLoaderOptions extends Partial<CoreOptions> {
}
export function register(options: NodeLoaderOptions = {}) {
module.register('./_loader.mjs', import.meta.url, {
module.register('./_loader.js', import.meta.url, {
data: options,
});
}
+67
View File
@@ -0,0 +1,67 @@
import { buildConfig } from '@/config/build';
import { _Defaults, createCore } from '@/core';
import { mdxLoaderGlob, metaLoaderGlob } from '@/loaders';
import { toVite } from '@/loaders/adapter';
import { createIntegratedConfigLoader } from '@/loaders/config';
import { createMdxLoader } from '@/loaders/mdx';
import { createMetaLoader } from '@/loaders/meta';
import fs from 'node:fs/promises';
import type { RolldownPlugin } from 'rolldown';
export interface PluginOptions {
configPath?: string;
outDir?: string;
}
export default async function mdx(
config: Record<string, unknown> | Promise<Record<string, unknown>>,
options?: PluginOptions,
): Promise<RolldownPlugin[]> {
const core = createCore({
configPath: options?.configPath ?? _Defaults.configPath,
outDir: options?.outDir ?? _Defaults.outDir,
environment: 'rolldown',
});
await core.init({
config: buildConfig(await config),
});
const configLoader = createIntegratedConfigLoader(core);
const mdxLoader = toVite(createMdxLoader(configLoader));
const metaLoader = toVite(
createMetaLoader(configLoader, {
// rolldown has built-in plugin for JSON files
json: 'json',
}),
);
return [
{
name: 'fumadocs-mdx',
load: {
filter: { id: [metaLoaderGlob, mdxLoaderGlob] },
// Rolldown couldn't read the correct file path when query params exist.
async handler(id) {
const idx = id.lastIndexOf('?');
if (idx === -1) return null;
return fs.readFile(id.slice(0, idx), 'utf-8');
},
},
},
{
name: 'fumadocs-mdx:mdx',
transform: {
filter: mdxLoader.filter,
handler: mdxLoader.transform,
},
},
{
name: 'fumadocs-mdx:meta',
transform: {
filter: metaLoader.filter,
handler: metaLoader.transform,
},
},
];
}
+39 -40
View File
@@ -1,6 +1,5 @@
import { runnerImport, type Plugin } from 'vite';
import { type PluginOption, runnerImport } from 'vite';
import { buildConfig } from '@/config/build';
import { ValidationError } from '@/utils/validation';
import { createMdxLoader } from '@/loaders/mdx';
import { toVite } from '@/loaders/adapter';
import type { FSWatcher } from 'chokidar';
@@ -40,7 +39,7 @@ export interface PluginOptions {
export default async function mdx(
_config?: Record<string, unknown> | Promise<Record<string, unknown>>,
pluginOptions: PluginOptions = {},
): Promise<Plugin> {
): Promise<PluginOption> {
const options = applyDefaults(pluginOptions);
const core = createViteCore(options);
const config =
@@ -58,47 +57,47 @@ export default async function mdx(
}),
);
return {
name: 'fumadocs-mdx',
// needed, otherwise other plugins will be executed before our `transform`.
enforce: 'pre',
async config() {
if (!options.updateViteConfig) return;
return [
{
name: 'fumadocs-mdx',
async config() {
if (!options.updateViteConfig) return;
const { getConfig } = await import('@fumadocs/vite');
return getConfig({ root: process.cwd() });
const { getConfig } = await import('@fumadocs/vite');
return getConfig({ root: process.cwd() });
},
async buildStart() {
await core.emit({ write: true });
},
async configureServer(server) {
await core.initServer({
watcher: server.watcher as unknown as FSWatcher,
});
},
},
async buildStart() {
await core.emit({ write: true });
{
name: 'fumadocs-mdx:mdx',
enforce: 'pre',
transform: {
filter: mdxLoader.filter,
async handler(code, id) {
// Vite RSC will pass the compiled MDX file's client module with ID `virtual:vite-rsc/client-references/group/facade:xxx.mdx`.
// The format of `value` becomes JavaScript, which will break the MDX compiler.
// We have to ignore them.
if (id.includes('virtual:vite-rsc')) return null;
return await mdxLoader.transform.call(this, code, id);
},
},
},
async configureServer(server) {
await core.initServer({
watcher: server.watcher as unknown as FSWatcher,
});
{
name: 'fumadocs-mdx:meta',
enforce: 'pre',
transform: {
filter: metaLoader.filter,
handler: metaLoader.transform,
},
},
async transform(value, id) {
// Vite RSC will pass the compiled MDX file's client module with ID `virtual:vite-rsc/client-references/group/facade:xxx.mdx`.
// The format of `value` becomes JavaScript, which will break the MDX compiler.
// We have to ignore them.
if (id.includes('virtual:vite-rsc')) return null;
try {
if (metaLoader.filter(id)) {
return await metaLoader.transform.call(this, value, id);
}
if (mdxLoader.filter(id)) {
return await mdxLoader.transform.call(this, value, id);
}
} catch (e) {
if (e instanceof ValidationError) {
throw new Error(await e.toStringFormatted(), { cause: e });
}
throw e;
}
},
};
];
}
export async function postInstall(pluginOptions: PluginOptions = {}) {
+1 -1
View File
@@ -3,7 +3,7 @@ import { defineConfig } from 'tsdown';
export default defineConfig({
entry: [
'./src/{index,bin}.ts',
'./src/{config,next,vite,bun}/index.ts',
'./src/{config,next,vite,bun,rolldown}/index.ts',
'./src/webpack/{mdx,meta}.ts',
'./src/node/{index,_loader,loader}.ts',
'./src/runtime/*.{ts,tsx}',