MDX: Support postprocess.includeMDAST option

This commit is contained in:
Fuma Nama
2025-10-19 23:18:40 +08:00
parent ad40e602ba
commit 90515746f5
10 changed files with 86 additions and 55 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-mdx': patch
---
Support `postprocess.includeMDAST` option
+4 -1
View File
@@ -16,8 +16,8 @@
"@ai-sdk/react": "^2.0.66",
"@fumadocs/mdx-remote": "workspace:*",
"@mixedbread/sdk": "^0.34.0",
"@orama/orama": "^3.1.15",
"@orama/core": "^1.2.13",
"@orama/orama": "^3.1.15",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-presence": "^1.1.5",
@@ -30,6 +30,8 @@
"ai": "^5.0.66",
"class-variance-authority": "^0.7.1",
"d3-force": "^3.0.0",
"estree-util-build-jsx": "^3.0.1",
"estree-util-to-js": "^2.0.0",
"feed": "^5.1.0",
"fumadocs-core": "workspace:*",
"fumadocs-docgen": "workspace:^",
@@ -64,6 +66,7 @@
"zod": "^4.1.12"
},
"devDependencies": {
"@types/estree": "^1.0.8",
"@fumadocs/cli": "workspace:*",
"@next/bundle-analyzer": "16.0.0-beta.0",
"@next/env": "16.0.0-beta.0",
+1 -1
View File
@@ -76,6 +76,7 @@
"estree-util-value-to-estree": "^3.4.0",
"js-yaml": "^4.1.0",
"lru-cache": "^11.2.2",
"unist-util-remove-position": "^5.0.0",
"mdast-util-to-markdown": "^2.1.2",
"picocolors": "^1.1.1",
"remark-mdx": "^3.1.1",
@@ -105,7 +106,6 @@
"remark-stringify": "^11.0.0",
"rollup": "^4.52.4",
"tsconfig": "workspace:*",
"unist-util-remove-position": "^5.0.0",
"vfile": "^6.0.3",
"vite": "^7.1.9",
"webpack": "^5.102.1"
@@ -48,6 +48,7 @@ export interface CompiledMDXProperties<Frontmatter = Record<string, unknown>> {
lastModified?: Date;
extractedReferences?: ExtractedReference[];
_markdown?: string;
_mdast?: string;
}
export interface FumadocsDataMap {
@@ -115,6 +116,7 @@ export async function buildMDX(
'frontmatter',
'lastModified',
'_markdown',
'_mdast',
],
} satisfies PostprocessOptions,
],
@@ -3,6 +3,7 @@ import type { Root, RootContent } from 'mdast';
import { visit } from 'unist-util-visit';
import { toMarkdown } from 'mdast-util-to-markdown';
import { valueToEstree } from 'estree-util-value-to-estree';
import { removePosition } from 'unist-util-remove-position';
export interface ExtractedReference {
href: string;
@@ -18,6 +19,15 @@ export interface PostprocessOptions {
* stringify MDAST and export via `_markdown`.
*/
includeProcessedMarkdown?: boolean;
/**
* store MDAST and export via `_mdast`.
*/
includeMDAST?:
| boolean
| {
removePosition?: boolean;
};
}
/**
@@ -28,6 +38,7 @@ export function remarkPostprocess(
this: Processor,
{
includeProcessedMarkdown = false,
includeMDAST = false,
valueToExport = [],
}: PostprocessOptions = {},
): Transformer<Root, Root> {
@@ -65,6 +76,14 @@ export function remarkPostprocess(
});
}
if (includeMDAST) {
const options = includeMDAST === true ? {} : includeMDAST;
file.data._mdast = JSON.stringify(
options.removePosition ? removePosition(structuredClone(tree)) : tree,
);
}
for (const { name, value } of file.data['mdx-export'] ?? []) {
tree.children.unshift(getMdastExport(name, value));
}
+5 -13
View File
@@ -5,10 +5,10 @@ import { buildMDX, CompiledMDXProperties } from '@/loaders/mdx/build-mdx';
import type { ProcessorOptions } from '@mdx-js/mdx';
import { executeMdx } from '@fumadocs/mdx-remote/client';
import { pathToFileURL } from 'node:url';
import { type DocData, missingProcessedMarkdown } from '@/runtime/shared';
import { createDocMethods, type DocData } from '@/runtime/shared';
import type { DocCollection } from '@/config';
import { readFileWithCache } from '@/next/map/file-cache';
import { fumaMatter } from '@/utils/fuma-matter';
import fs from 'node:fs/promises';
function getDocCollection(config: LoadedConfig, collection: string) {
const col = config.collections.get(collection);
@@ -36,7 +36,8 @@ export const _runtimeAsync: RuntimeAsync = {
async function compileAndLoad() {
if (cachedResult) return cachedResult;
const mdxOptions = await initMdxOptions;
const raw = await readFileWithCache(info.fullPath);
const raw = (await fs.readFile(info.fullPath)).toString();
const { content } = fumaMatter(raw);
const compiled = await buildMDX(collectionName, content, {
...mdxOptions,
@@ -57,16 +58,7 @@ export const _runtimeAsync: RuntimeAsync = {
return {
...data,
info,
async getText(type) {
if (type === 'raw') {
return readFileWithCache(info.fullPath);
}
const out = await compileAndLoad();
if (typeof out._markdown !== 'string') missingProcessedMarkdown();
return out._markdown;
},
...createDocMethods(info, () => compileAndLoad()),
async load() {
const out = await compileAndLoad();
+2 -12
View File
@@ -6,17 +6,14 @@ import {
} from 'fumadocs-core/source';
import type { DocOut, MetaOut, Runtime } from './types';
import type { CompiledMDXProperties } from '@/loaders/mdx/build-mdx';
import * as fs from 'node:fs/promises';
import { type FileInfo, missingProcessedMarkdown } from '@/runtime/shared';
import { createDocMethods, type FileInfo } from '@/runtime/shared';
export const _runtime: Runtime = {
doc(files) {
return files.map((file) => {
const data = file.data as unknown as CompiledMDXProperties;
const filePath = file.info.fullPath;
return {
info: file.info,
_exports: data as unknown as Record<string, unknown>,
body: data.default,
lastModified: data.lastModified,
@@ -24,14 +21,7 @@ export const _runtime: Runtime = {
structuredData: data.structuredData,
extractedReferences: data.extractedReferences,
...data.frontmatter,
async getText(type) {
if (type === 'raw') {
return (await fs.readFile(filePath)).toString();
}
if (typeof data._markdown !== 'string') missingProcessedMarkdown();
return data._markdown;
},
...createDocMethods(file.info, () => Promise.resolve(data)),
} satisfies DocOut;
}) as any;
},
+33 -4
View File
@@ -2,6 +2,9 @@ import type { StructuredData } from 'fumadocs-core/mdx-plugins';
import type { TOCItemType } from 'fumadocs-core/toc';
import type { MDXContent } from 'mdx/types';
import type { ExtractedReference } from '@/loaders/mdx/remark-postprocess';
import type { Root } from 'mdast';
import type { CompiledMDXProperties } from '@/loaders/mdx/build-mdx';
import fs from 'node:fs/promises';
export interface FileInfo {
/**
@@ -62,6 +65,8 @@ export interface DocMethods {
* - `type: processed` - get the processed Markdown content, only available when `includeProcessedMarkdown` is enabled on collection config.
*/
getText: (type: 'raw' | 'processed') => Promise<string>;
getMDAST: () => Promise<Root>;
}
export type MetaCollectionEntry<Data> = Data & {
@@ -79,8 +84,32 @@ export type AsyncDocCollectionEntry<Frontmatter> = DocMethods & {
load: () => Promise<DocData>;
} & Frontmatter;
export function missingProcessedMarkdown(): never {
throw new Error(
"getText('processed') requires `includeProcessedMarkdown` to be enabled in your collection config.",
);
export function createDocMethods(
info: FileInfo,
load: () => Promise<CompiledMDXProperties<any>>,
): DocMethods {
return {
info,
async getText(type) {
if (type === 'raw') {
return (await fs.readFile(info.fullPath)).toString();
}
const data = await load();
if (typeof data._markdown !== 'string')
throw new Error(
"getText('processed') requires `includeProcessedMarkdown` to be enabled in your collection config.",
);
return data._markdown;
},
async getMDAST() {
const data = await load();
if (!data._mdast)
throw new Error(
'getMDAST() requires `includeMDAST` to be enabled in your collection config.',
);
return JSON.parse(data._mdast);
},
};
}
+3 -21
View File
@@ -15,13 +15,12 @@ import {
import * as path from 'node:path';
import {
type AsyncDocCollectionEntry,
createDocMethods,
type DocCollectionEntry,
type DocData,
type FileInfo,
type MetaCollectionEntry,
missingProcessedMarkdown,
} from '@/runtime/shared';
import fs from 'node:fs/promises';
export * from './base';
@@ -73,16 +72,8 @@ export function fromConfig<Config>(): ServerCreate<Config> {
): DocCollectionEntry<Frontmatter> {
return {
...mapDocData(entry),
info,
async getText(type) {
if (type === 'raw') {
return (await fs.readFile(info.fullPath)).toString();
}
if (typeof entry._markdown !== 'string') missingProcessedMarkdown();
return entry._markdown;
},
...entry.frontmatter,
...createDocMethods(info, async () => entry),
};
}
@@ -93,19 +84,10 @@ export function fromConfig<Config>(): ServerCreate<Config> {
): AsyncDocCollectionEntry<Frontmatter> {
return {
...head,
info,
...createDocMethods(info, content),
async load() {
return mapDocData(await content());
},
async getText(type) {
if (type === 'raw') {
return (await fs.readFile(info.fullPath)).toString();
}
const entry = await content();
if (typeof entry._markdown !== 'string') missingProcessedMarkdown();
return entry._markdown;
},
};
}
+12 -3
View File
@@ -113,6 +113,12 @@ importers:
d3-force:
specifier: ^3.0.0
version: 3.0.0
estree-util-build-jsx:
specifier: ^3.0.1
version: 3.0.1
estree-util-to-js:
specifier: ^2.0.0
version: 2.0.0
feed:
specifier: ^5.1.0
version: 5.1.0
@@ -228,6 +234,9 @@ importers:
'@types/d3-force':
specifier: ^3.0.10
version: 3.0.10
'@types/estree':
specifier: ^1.0.8
version: 1.0.8
'@types/hast':
specifier: ^3.0.4
version: 3.0.4
@@ -1573,6 +1582,9 @@ importers:
unified:
specifier: ^11.0.5
version: 11.0.5
unist-util-remove-position:
specifier: ^5.0.0
version: 5.0.0
unist-util-visit:
specifier: ^5.0.0
version: 5.0.0
@@ -1634,9 +1646,6 @@ importers:
tsconfig:
specifier: workspace:*
version: link:../tsconfig
unist-util-remove-position:
specifier: ^5.0.0
version: 5.0.0
vfile:
specifier: ^6.0.3
version: 6.0.3