MDX: Support Async Mode for Vite

This commit is contained in:
Fuma Nama
2025-08-21 17:23:40 +08:00
parent 67df155f0e
commit cfe2a5c04e
7 changed files with 186 additions and 133 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-mdx': minor
---
Support Async Mode for Vite
+4 -5
View File
@@ -40,16 +40,15 @@ export const _runtimeAsync: RuntimeAsync = {
},
filePath: file.absolutePath,
});
const executed = await executeMdx(String(out), {
const { default: body, ...rest } = await executeMdx(String(out), {
baseUrl: pathToFileURL(file.absolutePath),
});
return {
body: executed.default,
toc: executed.toc,
lastModified,
structuredData: out.data.structuredData as StructuredData,
_exports: executed,
body,
...rest,
lastModified,
};
},
};
+6 -2
View File
@@ -23,7 +23,6 @@ export interface MarkdownProps {
body: FC<MDXProps>;
structuredData: StructuredData;
toc: TableOfContents;
_exports: Record<string, unknown>;
/**
* Only available when `lastModifiedTime` is enabled on MDX loader
@@ -45,6 +44,11 @@ export interface AsyncRuntimeFile {
type DocOut<Schema extends StandardSchemaV1> = Override<
MarkdownProps & {
/**
* Other exports in the compiled Markdown/MDX file
*/
_exports: Record<string, unknown>;
/**
* Read the original content of file from file system.
*/
@@ -89,7 +93,7 @@ type AsyncDocOut<Schema extends StandardSchemaV1> =
StandardSchemaV1.InferOutput<Schema> &
BaseCollectionEntry & {
content: string;
load: () => Promise<MarkdownProps>;
load: () => Promise<MarkdownProps & Record<string, unknown>>;
};
export interface RuntimeAsync {
+111 -90
View File
@@ -26,66 +26,68 @@ interface CompiledMDXProperties<Frontmatter> {
export type CompiledMDXFile<Frontmatter> = CompiledMDXProperties<Frontmatter> &
Record<string, unknown>;
type MDXFileToPageData<Frontmatter> = Frontmatter &
Omit<CompiledMDXProperties<Frontmatter>, 'frontmatter'> & {
_exports: Record<string, unknown>;
};
type Override<A, B> = Omit<A, keyof B> & B;
type AttachGlobValue<GlobValue, Attach> =
GlobValue extends () => Promise<unknown> ? () => Promise<Attach> : Attach;
type MDXFileToPageData<Frontmatter> = Override<
Omit<CompiledMDXProperties<Frontmatter>, 'frontmatter' | 'default'>,
Frontmatter & {
/**
* @deprecated use `body` instead.
*/
default: FC<MDXProps>;
_exports: Record<string, unknown>;
body: FC<MDXProps>;
}
>;
type MDXFileToPageDataLazy<Frontmatter> = Override<
Frontmatter,
{
load: () => Promise<
Omit<CompiledMDXFile<Frontmatter>, 'default'> & {
body: FC<MDXProps>;
}
>;
}
>;
interface LazyDocMap<Frontmatter> {
head: Record<string, () => Promise<Frontmatter>>;
body: Record<string, () => Promise<CompiledMDXFile<Frontmatter>>>;
}
export function fromConfig<Config>(): {
doc: <Name extends keyof Config, GlobValue>(
doc: <Name extends keyof Config>(
name: Name,
glob: Record<string, GlobValue>,
) => Config[Name] extends DocCollection<infer Schema>
glob: Record<string, () => Promise<unknown>>,
) => Config[Name] extends
| DocCollection<infer Schema>
| DocsCollection<infer Schema>
? Record<
string,
AttachGlobValue<
GlobValue,
CompiledMDXFile<StandardSchemaV1.InferOutput<Schema>>
>
() => Promise<CompiledMDXFile<StandardSchemaV1.InferOutput<Schema>>>
>
: never;
meta: <Name extends keyof Config, GlobValue>(
docLazy: <Name extends keyof Config>(
name: Name,
glob: Record<string, GlobValue>,
) => Config[Name] extends MetaCollection<infer Schema>
? AttachGlobValue<GlobValue, StandardSchemaV1.InferOutput<Schema>>
headGlob: Record<string, () => Promise<unknown>>,
bodyGlob: Record<string, () => Promise<unknown>>,
) => Config[Name] extends
| DocCollection<infer Schema>
| DocsCollection<infer Schema>
? LazyDocMap<StandardSchemaV1.InferOutput<Schema>>
: never;
docs: <Name extends keyof Config, DocGlobValue, MetaGlobValue>(
meta: <Name extends keyof Config>(
name: Name,
options: {
meta: Record<string, MetaGlobValue>;
doc: Record<string, DocGlobValue>;
},
) => Config[Name] extends DocsCollection<infer DocSchema, infer MetaSchema>
? {
doc: Record<
string,
AttachGlobValue<
DocGlobValue,
CompiledMDXFile<StandardSchemaV1.InferOutput<DocSchema>>
>
>;
meta: Record<
string,
AttachGlobValue<
MetaGlobValue,
StandardSchemaV1.InferOutput<MetaSchema>
>
>;
}
glob: Record<string, () => Promise<unknown>>,
) => Config[Name] extends
| MetaCollection<infer Schema>
| DocsCollection<StandardSchemaV1, infer Schema>
? Record<string, () => Promise<StandardSchemaV1.InferOutput<Schema>>>
: never;
source: <DocOut extends PageData, MetaOut extends MetaData>(
doc: Record<string, CompiledMDXFile<DocOut>>,
meta: Record<string, MetaOut>,
) => Source<{
pageData: MDXFileToPageData<DocOut>;
metaData: MetaOut;
}>;
sourceAsync: <DocOut extends PageData, MetaOut extends MetaData>(
doc: Record<string, () => Promise<CompiledMDXFile<DocOut>>>,
@@ -96,6 +98,16 @@ export function fromConfig<Config>(): {
metaData: MetaOut;
}>
>;
sourceLazy: <DocOut extends PageData, MetaOut extends MetaData>(
doc: LazyDocMap<DocOut>,
meta: Record<string, () => Promise<MetaOut>>,
) => Promise<
Source<{
pageData: MDXFileToPageDataLazy<DocOut>;
metaData: MetaOut;
}>
>;
} {
function normalize(entries: Record<string, unknown>) {
const out: Record<string, unknown> = {};
@@ -114,6 +126,7 @@ export function fromConfig<Config>(): {
return {
...frontmatter,
default: entry.default,
body: entry.default,
toc,
structuredData,
lastModified,
@@ -121,6 +134,19 @@ export function fromConfig<Config>(): {
};
}
function mapPageDataLazy<Frontmatter>(
head: Frontmatter,
content: () => Promise<CompiledMDXFile<Frontmatter>>,
): MDXFileToPageDataLazy<Frontmatter> {
return {
...head,
async load() {
const { default: body, ...rest } = await content();
return { body, ...rest };
},
};
}
return {
doc(_, glob) {
return normalize(glob) as any;
@@ -128,56 +154,51 @@ export function fromConfig<Config>(): {
meta(_, glob) {
return normalize(glob) as any;
},
docs(_, { doc, meta }) {
docLazy(_, head, body) {
return {
doc: normalize(doc),
meta: normalize(meta),
head: normalize(head),
body: normalize(body),
} as any;
},
source(doc, meta) {
const virtualFiles: VirtualFile[] = [];
for (const [file, content] of Object.entries(doc)) {
virtualFiles.push({
type: 'page',
path: file,
data: mapPageData(content),
});
}
for (const [file, content] of Object.entries(meta)) {
virtualFiles.push({
type: 'meta',
path: file,
data: content,
});
}
return {
files: virtualFiles,
};
},
async sourceAsync(doc, meta) {
const virtualFiles: VirtualFile[] = [];
const virtualFiles: Promise<VirtualFile>[] = [
...Object.entries(doc).map(async ([file, content]) => {
return {
type: 'page',
path: file,
data: mapPageData(await content()),
} satisfies VirtualFile;
}),
...Object.entries(meta).map(async ([file, content]) => {
return {
type: 'meta',
path: file,
data: await content(),
} satisfies VirtualFile;
}),
];
for (const [file, content] of Object.entries(doc)) {
virtualFiles.push({
type: 'page',
path: file,
data: mapPageData(await content()),
});
}
return { files: await Promise.all(virtualFiles) };
},
async sourceLazy(doc, meta) {
const virtualFiles: Promise<VirtualFile>[] = [
...Object.entries(doc.head).map(async ([file, frontmatter]) => {
return {
type: 'page',
path: file,
data: mapPageDataLazy(await frontmatter(), doc.body[file]),
} satisfies VirtualFile;
}),
...Object.entries(meta).map(async ([file, content]) => {
return {
type: 'meta',
path: file,
data: await content(),
} satisfies VirtualFile;
}),
];
for (const [file, content] of Object.entries(meta)) {
virtualFiles.push({
type: 'meta',
path: file,
data: await content(),
});
}
return {
files: virtualFiles,
};
return { files: await Promise.all(virtualFiles) };
},
};
}
+11 -5
View File
@@ -1,22 +1,28 @@
import type { AnyCollection, DocCollection, MetaCollection } from '@/config';
import { getGlobPatterns } from '@/utils/collections';
interface GlobOptions {
query: Record<string, string>;
base?: string;
eager?: boolean;
import?: string;
}
export function generateGlob(
name: string,
collection: MetaCollection | DocCollection,
globOptions?: Partial<GlobOptions>,
) {
const patterns = mapGlobPatterns(getGlobPatterns(collection));
const options: Record<string, unknown> = {
const options: GlobOptions = {
...globOptions,
query: {
...globOptions?.query,
collection: name,
},
base: getGlobBase(collection),
};
if (collection.type === 'meta') {
options.import = 'default';
}
return `import.meta.glob(${JSON.stringify(patterns)}, ${JSON.stringify(options, null, 2)})`;
}
+31
View File
@@ -0,0 +1,31 @@
import type { DocCollection, DocsCollection, MetaCollection } from '@/config';
import { ident } from '@/utils/import-formatter';
import { generateGlob } from '@/vite/generate-glob';
export function docs(name: string, collection: DocsCollection) {
const obj = [
ident(`doc: ${doc(name, collection.docs)}`),
ident(`meta: ${meta(name, collection.meta)}`),
].join(',\n');
return `{\n${obj}\n}`;
}
export function doc(name: string, collection: DocCollection) {
if (collection.async) {
return `create.docLazy("${name}", ${generateGlob(name, collection, {
query: {
only: 'frontmatter',
},
import: 'frontmatter',
})}, ${generateGlob(name, collection)})`;
}
return `create.doc("${name}", ${generateGlob(name, collection)})`;
}
export function meta(name: string, collection: MetaCollection) {
return `create.meta("${name}", ${generateGlob(name, collection, {
import: 'default',
})})`;
}
+18 -31
View File
@@ -6,16 +6,20 @@ import { countLines } from '@/utils/count-lines';
import { fumaMatter } from '@/utils/fuma-matter';
import { validate, ValidationError } from '@/utils/validation';
import { z } from 'zod';
import { ident, toImportPath } from '@/utils/import-formatter';
import type { DocCollection, DocsCollection, MetaCollection } from '@/config';
import { toImportPath } from '@/utils/import-formatter';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { load } from 'js-yaml';
import type { SourceMap, TransformPluginContext } from 'rollup';
import { generateGlob } from '@/vite/generate-glob';
import { getGitTimestamp } from '@/utils/git-timestamp';
import { doc, docs, meta } from '@/vite/generate';
const onlySchema = z.literal(['frontmatter', 'all']);
const querySchema = z
.object({
only: z.literal(['frontmatter', 'all']).default('all'),
collection: z.string().optional(),
})
.loose();
export interface PluginOptions {
/**
@@ -100,15 +104,11 @@ export default function mdx(
): Promise<TransformResult | null> {
const matter = fumaMatter(value);
const isDevelopment = this.environment.mode === 'dev';
const parsed = parse(query) as {
collection?: string;
only?: string;
};
const parsed = querySchema.parse(parse(query));
const collection = parsed.collection
? loaded.collections.get(parsed.collection)
: undefined;
const only = parsed.only ? onlySchema.parse(parsed.only) : 'all';
let schema;
let mdxOptions;
@@ -135,7 +135,7 @@ export default function mdx(
);
}
if (only === 'frontmatter') {
if (parsed.only === 'frontmatter') {
return {
code: `export const frontmatter = ${JSON.stringify(matter.data)}`,
map: null,
@@ -199,32 +199,19 @@ export default function mdx(
`export const create = fromConfig<typeof Config>();`,
];
function docs(name: string, collection: DocsCollection) {
const args = [
ident(`doc: ${generateGlob(name, collection.docs)}`),
ident(`meta: ${generateGlob(name, collection.meta)}`),
].join(',\n');
return `export const ${name} = create.docs("${name}", {\n${args}\n});`;
}
function doc(name: string, collection: DocCollection) {
return `export const ${name} = create.doc("${name}", ${generateGlob(name, collection)});`;
}
function meta(name: string, collection: MetaCollection) {
return `export const ${name} = create.meta("${name}", ${generateGlob(name, collection)});`;
}
for (const [name, collection] of loaded.collections.entries()) {
lines.push('');
let body: string;
if (collection.type === 'docs') {
lines.push(docs(name, collection));
body = docs(name, collection);
} else if (collection.type === 'meta') {
lines.push(meta(name, collection));
body = meta(name, collection);
} else {
lines.push(doc(name, collection));
body = doc(name, collection);
}
lines.push('');
lines.push(`export const ${name} = ${body};`);
}
await fs.writeFile(path.join(outdir, outFile), lines.join('\n'));