MDX: Support runtime: bun in Vite index file generation

This commit is contained in:
Fuma Nama
2025-10-13 22:22:03 +08:00
parent 5966e23c29
commit a5df956448
9 changed files with 187 additions and 126 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-mdx': patch
---
Support `runtime: bun` in Vite index file generation
+3 -3
View File
@@ -3,7 +3,7 @@
import { existsSync } from 'node:fs';
async function start() {
const args = process.argv.slice(2);
const [configPath] = process.argv.slice(2);
const isNext =
existsSync('next.config.js') ||
existsSync('next.config.mjs') ||
@@ -11,10 +11,10 @@ async function start() {
if (isNext) {
const { postInstall } = await import('./next');
await postInstall(...args);
await postInstall(configPath);
} else {
const { postInstall } = await import('./vite/postinstall');
await postInstall(...args);
await postInstall(configPath);
}
}
+39
View File
@@ -0,0 +1,39 @@
import { globSync } from 'tinyglobby';
import { pathToFileURL } from 'node:url';
import path from 'node:path';
import type { GlobOptions } from '@/vite/generate';
export function importMetaGlob(
patterns: string | string[],
options: GlobOptions,
) {
const baseDir = options.base;
if (!baseDir) {
throw new Error(
"[import.meta.glob polyfill] base option is required as it cannot access the caller's import.meta.url",
);
}
const result = globSync(patterns, {
cwd: baseDir,
});
const imports: Record<string, () => Promise<unknown>> = {};
for (const item of result) {
const fullPath = path.join(baseDir, item);
const url = pathToFileURL(fullPath);
for (const [k, v] of Object.entries(options.query ?? {})) {
url.searchParams.set(k, v);
}
imports[item] = async () => {
const mod = await import(url.href);
if (options.import) return mod[options.import];
return mod;
};
}
return imports;
}
+2
View File
@@ -47,3 +47,5 @@ export function createMdxPlugin(options: MdxPluginOptions = {}): BunPlugin {
},
};
}
export * from './glob';
+1 -4
View File
@@ -55,10 +55,7 @@ export async function loadConfig(
const url = pathToFileURL(path.resolve(outDir, 'source.config.mjs'));
const config = import(`${url.href}?hash=${hash}`).then((loaded) => {
return buildConfig(
// every call to `loadConfig` will cause the previous cache to be ignored
loaded as Record<string, unknown>,
);
return buildConfig(loaded as Record<string, unknown>);
});
if (hash) cache = { config, hash };
-50
View File
@@ -1,50 +0,0 @@
import type { AnyCollection } from '@/config';
interface GlobOptions {
query: Record<string, string>;
base?: string;
eager?: boolean;
import?: string;
}
export function generateGlob(
name: string,
patterns: string[],
globOptions?: Partial<GlobOptions>,
) {
const options: GlobOptions = {
...globOptions,
query: {
...globOptions?.query,
collection: name,
},
};
return `import.meta.glob(${JSON.stringify(mapGlobPatterns(patterns))}, ${JSON.stringify(options, null, 2)})`;
}
function mapGlobPatterns(patterns: string[]) {
return patterns.map(enforceRelative);
}
function enforceRelative(file: string) {
if (file.startsWith('./')) return file;
if (file.startsWith('/')) return `.${file}`;
return `./${file}`;
}
export function getGlobBase(collection: AnyCollection) {
let dir = collection.dir;
if (Array.isArray(dir)) {
if (dir.length !== 1)
throw new Error(
`[Fumadocs MDX] Vite Plugin doesn't support multiple \`dir\` for a collection at the moment.`,
);
dir = dir[0];
}
return enforceRelative(dir);
}
+123 -40
View File
@@ -1,67 +1,124 @@
import type { DocCollection, DocsCollection, MetaCollection } from '@/config';
import type {
AnyCollection,
DocCollection,
DocsCollection,
MetaCollection,
} from '@/config';
import { ident, toImportPath } from '@/utils/import-formatter';
import { generateGlob, getGlobBase } from '@/vite/generate-glob';
import type { LoadedConfig } from '@/loaders/config';
import { getGlobPatterns } from '@/utils/collections';
import path from 'node:path';
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 interface GlobOptions {
query: Record<string, string>;
base?: string;
import?: string;
}
function doc(name: string, collection: DocCollection) {
const patterns = getGlobPatterns(collection);
const base = getGlobBase(collection);
const docGlob = generateGlob(name, patterns, {
base,
});
export interface IndexFileOptions {
/**
* Runtime compat fallbacks
*/
runtime?: 'bun';
if (collection.async) {
const headBlob = generateGlob(name, patterns, {
query: {
only: 'frontmatter',
},
import: 'frontmatter',
base,
});
/**
* Output index file path
*
* @defaultValue 'source.generated.ts'
*/
out?: string;
return `create.docLazy("${name}", "${base}", ${headBlob}, ${docGlob})`;
}
return `create.doc("${name}", "${base}", ${docGlob})`;
}
function meta(name: string, collection: MetaCollection) {
const patterns = getGlobPatterns(collection);
const base = getGlobBase(collection);
return `create.meta("${name}", "${base}", ${generateGlob(name, patterns, {
import: 'default',
base,
})})`;
/**
* add `.js` extensions to imports, needed for ESM without bundler resolution
*/
addJsExtension?: boolean;
}
export function entry(
configPath: string,
config: LoadedConfig,
outDir: string,
jsExtension?: boolean,
options: IndexFileOptions,
) {
const {
out = 'source.generated.ts',
addJsExtension = false,
runtime,
} = options;
const outDir = path.dirname(out);
const lines = [
'/// <reference types="vite/client" />',
`import { fromConfig } from 'fumadocs-mdx/runtime/vite';`,
`import type * as Config from '${toImportPath(configPath, {
relativeTo: outDir,
jsExtension,
jsExtension: addJsExtension,
})}';`,
'',
`export const create = fromConfig<typeof Config>();`,
];
if (runtime === 'bun') {
lines.push(`import { importMetaGlob } from 'fumadocs-mdx/bun';`);
}
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}`;
}
function doc(name: string, collection: DocCollection) {
const patterns = getGlobPatterns(collection);
const base = getGlobBase(collection);
const docGlob = generateGlob(name, patterns, {
base,
});
if (collection.async) {
const headBlob = generateGlob(name, patterns, {
query: {
only: 'frontmatter',
},
import: 'frontmatter',
base,
});
return `create.docLazy("${name}", "${base}", ${headBlob}, ${docGlob})`;
}
return `create.doc("${name}", "${base}", ${docGlob})`;
}
function meta(name: string, collection: MetaCollection) {
const patterns = getGlobPatterns(collection);
const base = getGlobBase(collection);
return `create.meta("${name}", "${base}", ${generateGlob(name, patterns, {
import: 'default',
base,
})})`;
}
function generateGlob(
name: string,
patterns: string[],
globOptions?: Partial<GlobOptions>,
) {
const options: GlobOptions = {
...globOptions,
query: {
...globOptions?.query,
collection: name,
},
};
const fnName = runtime === 'bun' ? 'importMetaGlob' : 'import.meta.glob';
return `${fnName}(${JSON.stringify(mapGlobPatterns(patterns))}, ${JSON.stringify(options, null, 2)})`;
}
for (const [name, collection] of config.collections.entries()) {
let body: string;
@@ -79,3 +136,29 @@ export function entry(
return lines.join('\n');
}
function mapGlobPatterns(patterns: string[]) {
return patterns.map(enforceRelative);
}
function enforceRelative(file: string) {
if (file.startsWith('./')) return file;
if (file.startsWith('/')) return `.${file}`;
return `./${file}`;
}
function getGlobBase(collection: AnyCollection) {
let dir = collection.dir;
if (Array.isArray(dir)) {
if (dir.length !== 1)
throw new Error(
`[Fumadocs MDX] Vite Plugin doesn't support multiple \`dir\` for a collection at the moment.`,
);
dir = dir[0];
}
return enforceRelative(dir);
}
+9 -17
View File
@@ -10,7 +10,7 @@ import { validate, ValidationError } from '@/utils/validation';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { load } from 'js-yaml';
import { entry } from '@/vite/generate';
import { entry, type IndexFileOptions } from '@/vite/generate';
import { createMdxLoader } from '@/loaders/mdx';
import { resolvedConfig } from '@/loaders/config';
import { toVite } from '@/loaders/adapter';
@@ -23,15 +23,7 @@ export interface PluginOptions {
*
* @defaultValue true
*/
generateIndexFile?:
| boolean
| {
out?: string;
/**
* add `.js` extensions to imports, needed for ESM without bundler resolution
*/
addJsExtension?: boolean;
};
generateIndexFile?: boolean | IndexFileOptions;
/**
* @defaultValue source.config.ts
@@ -126,15 +118,15 @@ export default function mdx(
} satisfies UserConfig);
},
async buildStart() {
if (!generateIndexFile) return;
const { out = 'source.generated.ts', addJsExtension } =
typeof generateIndexFile === 'object' ? generateIndexFile : {};
let indexFileOptions = generateIndexFile;
if (!indexFileOptions) return;
if (typeof indexFileOptions !== 'object') indexFileOptions = {};
const { out = 'source.generated.ts' } = indexFileOptions;
console.log('[Fumadocs MDX] Generating index files');
const dir = path.dirname(out);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(out, entry(configPath, loaded, dir, addJsExtension));
await fs.mkdir(path.dirname(out), { recursive: true });
await fs.writeFile(out, entry(configPath, loaded, indexFileOptions));
},
async transform(value, id) {
+5 -12
View File
@@ -1,24 +1,17 @@
import { loadConfig } from '@/loaders/config/load';
import fs from 'node:fs/promises';
import path from 'node:path';
import { entry } from '@/vite/generate';
import { entry, type IndexFileOptions } from '@/vite/generate';
import { findConfigFile } from '@/loaders/config';
export async function postInstall(
configPath = findConfigFile(),
outDir?: string,
addJsExtension = false,
options: IndexFileOptions = {},
) {
const { out = 'source.generated.ts' } = options;
const config = await loadConfig(configPath, 'node_modules', undefined, true);
const outFile = 'source.generated.ts';
if (outDir) {
await fs.mkdir(outDir, { recursive: true });
}
await fs.writeFile(
outDir ? path.join(outDir, outFile) : outFile,
entry(configPath, config, outDir ?? process.cwd(), addJsExtension),
);
await fs.mkdir(path.dirname(out), { recursive: true });
await fs.writeFile(out, entry(configPath, config, options));
console.log('[MDX] types generated');
}