fumadocs-mdx
Version:
The built-in source for Fumadocs
808 lines (787 loc) • 24.3 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/mdx-plugins/remark-exports.ts
function remarkMdxExport({ values }) {
return (tree, vfile) => {
for (const name of values) {
if (!(name in vfile.data)) return;
tree.children.unshift(getMdastExport(name, vfile.data[name]));
}
};
}
function getMdastExport(name, value) {
return {
type: "mdxjsEsm",
value: "",
data: {
estree: {
type: "Program",
sourceType: "module",
body: [
{
type: "ExportNamedDeclaration",
specifiers: [],
source: null,
declaration: {
type: "VariableDeclaration",
kind: "let",
declarations: [
{
type: "VariableDeclarator",
id: {
type: "Identifier",
name
},
init: (0, import_estree_util_value_to_estree.valueToEstree)(value)
}
]
}
}
]
}
}
};
}
var import_estree_util_value_to_estree;
var init_remark_exports = __esm({
"src/mdx-plugins/remark-exports.ts"() {
"use strict";
import_estree_util_value_to_estree = require("estree-util-value-to-estree");
}
});
// src/utils/mdx-options.ts
var mdx_options_exports = {};
__export(mdx_options_exports, {
getDefaultMDXOptions: () => getDefaultMDXOptions
});
function pluginOption(def, options = []) {
const list = def(Array.isArray(options) ? options : []).filter(
Boolean
);
if (typeof options === "function") {
return options(list);
}
return list;
}
function getDefaultMDXOptions({
valueToExport = [],
rehypeCodeOptions,
remarkImageOptions,
remarkHeadingOptions,
remarkStructureOptions,
remarkCodeTabOptions,
remarkNpmOptions,
_withoutBundler = false,
...mdxOptions
}) {
const mdxExports = [
"structuredData",
"frontmatter",
"lastModified",
...valueToExport
];
const remarkPlugins = pluginOption(
(v) => [
plugins.remarkGfm,
[
plugins.remarkHeading,
{
generateToc: false,
...remarkHeadingOptions
}
],
remarkImageOptions !== false && [
plugins.remarkImage,
{
...remarkImageOptions,
useImport: _withoutBundler ? false : remarkImageOptions?.useImport
}
],
"remarkCodeTab" in plugins && remarkCodeTabOptions !== false && [
plugins.remarkCodeTab,
remarkCodeTabOptions
],
"remarkNpm" in plugins && remarkNpmOptions !== false && [plugins.remarkNpm, remarkNpmOptions],
...v,
remarkStructureOptions !== false && [
plugins.remarkStructure,
remarkStructureOptions
],
[remarkMdxExport, { values: mdxExports }]
],
mdxOptions.remarkPlugins
);
const rehypePlugins = pluginOption(
(v) => [
rehypeCodeOptions !== false && [plugins.rehypeCode, rehypeCodeOptions],
...v,
plugins.rehypeToc
],
mdxOptions.rehypePlugins
);
return {
...mdxOptions,
outputFormat: _withoutBundler ? "function-body" : mdxOptions.outputFormat,
remarkPlugins,
rehypePlugins
};
}
var plugins;
var init_mdx_options = __esm({
"src/utils/mdx-options.ts"() {
"use strict";
plugins = __toESM(require("fumadocs-core/mdx-plugins"), 1);
init_remark_exports();
}
});
// src/map/watcher.ts
var watcher_exports = {};
__export(watcher_exports, {
watcher: () => watcher
});
function watcher(configPath, config, ignored) {
const watcher2 = new import_chokidar.FSWatcher({
ignoreInitial: true,
persistent: true,
ignored
});
watcher2.add(configPath);
for (const collection of config.collections.values()) {
if (collection.type === "docs") {
watcher2.add(collection.docs.dir);
watcher2.add(collection.meta.dir);
} else {
watcher2.add(collection.dir);
}
}
return watcher2;
}
var import_chokidar;
var init_watcher = __esm({
"src/map/watcher.ts"() {
"use strict";
import_chokidar = require("chokidar");
}
});
// src/next/index.ts
var next_exports = {};
__export(next_exports, {
createMDX: () => createMDX,
postInstall: () => postInstall,
start: () => start
});
module.exports = __toCommonJS(next_exports);
// src/utils/config.ts
var fs = __toESM(require("fs/promises"), 1);
var path = __toESM(require("path"), 1);
var import_node_url = require("url");
// src/config/build.ts
function buildConfig(config) {
const collections = /* @__PURE__ */ new Map();
let globalConfig = {};
for (const [k, v] of Object.entries(config)) {
if (!v) {
continue;
}
if (typeof v === "object" && "type" in v) {
if (v.type === "docs") {
collections.set(k, v);
continue;
}
if (v.type === "doc" || v.type === "meta") {
collections.set(k, v);
continue;
}
}
if (k === "default" && v) {
globalConfig = v;
continue;
}
throw new Error(
`Unknown export "${k}", you can only export collections from source configuration file.`
);
}
const mdxOptionsCache = /* @__PURE__ */ new Map();
return {
global: globalConfig,
collections,
async getDefaultMDXOptions(mode = "default") {
const cached = mdxOptionsCache.get(mode);
if (cached) return cached;
const input = this.global.mdxOptions;
async function uncached() {
const options = typeof input === "function" ? await input() : input;
const { getDefaultMDXOptions: getDefaultMDXOptions2 } = await Promise.resolve().then(() => (init_mdx_options(), mdx_options_exports));
if (options?.preset === "minimal") return options;
return getDefaultMDXOptions2({
...options,
_withoutBundler: mode === "remote"
});
}
const result = uncached();
mdxOptionsCache.set(mode, result);
return result;
}
};
}
// src/utils/config.ts
function findConfigFile() {
return path.resolve("source.config.ts");
}
var cache = null;
async function isZod3() {
try {
const content = JSON.parse(
(await fs.readFile("node_modules/zod/package.json")).toString()
);
const version = content.version;
return typeof version === "string" && version.startsWith("3.");
} catch {
return false;
}
}
function createCompatZodPlugin() {
return {
name: "replace-zod-import",
async setup(build) {
const usingZod3 = await isZod3();
if (!usingZod3) return;
console.warn(
"[Fumadocs MDX] Noticed Zod v3 in your node_modules, we recommend upgrading to Zod v4 for better compatibility."
);
build.onResolve({ filter: /^fumadocs-mdx\/config$/ }, () => {
return {
path: "fumadocs-mdx/config/zod-3",
external: true
};
});
}
};
}
async function compileConfig(configPath, outDir) {
const { build } = await import("esbuild");
const transformed = await build({
entryPoints: [{ in: configPath, out: "source.config" }],
bundle: true,
outdir: outDir,
target: "node20",
write: true,
platform: "node",
format: "esm",
packages: "external",
plugins: [createCompatZodPlugin()],
outExtension: {
".js": ".mjs"
},
allowOverwrite: true
});
if (transformed.errors.length > 0) {
throw new Error("failed to compile configuration file");
}
}
async function loadConfig(configPath, outDir, hash, build = false) {
if (cache && cache.hash === hash) {
return await cache.config;
}
if (build) await compileConfig(configPath, outDir);
const url = (0, import_node_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
);
});
cache = { config, hash };
return await config;
}
async function getConfigHash(configPath) {
const stats = await fs.stat(configPath).catch(() => void 0);
if (stats) {
return stats.mtime.getTime().toString();
}
throw new Error("Cannot find config file");
}
// src/map/index.ts
var path5 = __toESM(require("path"), 1);
var fs3 = __toESM(require("fs/promises"), 1);
// src/map/generate.ts
var path4 = __toESM(require("path"), 1);
var fs2 = __toESM(require("fs/promises"), 1);
var import_tinyglobby = require("tinyglobby");
// src/utils/validation.ts
var import_picocolors = __toESM(require("picocolors"), 1);
var ValidationError = class extends Error {
constructor(message, issues) {
super(
`${message}:
${issues.map((issue) => ` ${issue.path}: ${issue.message}`).join("\n")}`
);
this.title = message;
this.issues = issues;
}
toStringFormatted() {
return [
import_picocolors.default.bold(`[MDX] ${this.title}:`),
...this.issues.map(
(issue) => import_picocolors.default.redBright(
`- ${import_picocolors.default.bold(issue.path?.join(".") ?? "*")}: ${issue.message}`
)
)
].join("\n");
}
};
async function validate(schema, data, context, errorMessage) {
if (typeof schema === "function" && !("~standard" in schema)) {
schema = schema(context);
}
if ("~standard" in schema) {
const result = await schema["~standard"].validate(
data
);
if (result.issues) {
throw new ValidationError(errorMessage, result.issues);
}
return result.value;
}
return data;
}
// src/map/file-cache.ts
var import_lru_cache = require("lru-cache");
var map = new import_lru_cache.LRUCache({
max: 200
});
var fileCache = {
read(namespace, path7) {
return map.get(`${namespace}.${path7}`);
},
write(namespace, path7, data) {
map.set(`${namespace}.${path7}`, data);
},
removeCache(path7) {
for (const key of map.keys()) {
const keyPath = key.slice(key.indexOf(".") + 1);
if (keyPath === path7) map.delete(key);
}
}
};
// src/map/generate.ts
var import_js_yaml2 = require("js-yaml");
// src/utils/git-timestamp.ts
var import_node_path = __toESM(require("path"), 1);
var import_tinyexec = require("tinyexec");
var cache2 = /* @__PURE__ */ new Map();
async function getGitTimestamp(file) {
const cached = cache2.get(file);
if (cached) return cached;
try {
const out = await (0, import_tinyexec.x)(
"git",
["log", "-1", '--pretty="%ai"', import_node_path.default.relative(process.cwd(), file)],
{
throwOnError: true
}
);
const time = new Date(out.stdout);
cache2.set(file, time);
return time;
} catch {
return;
}
}
// src/utils/fuma-matter.ts
var import_js_yaml = require("js-yaml");
var regex = /^---\r?\n(.+?)\r?\n---\r?\n/s;
function fumaMatter(input) {
const output = { matter: "", data: {}, content: input };
const match = regex.exec(input);
if (!match) {
return output;
}
output.matter = match[1];
output.content = input.slice(match[0].length);
const loaded = (0, import_js_yaml.load)(output.matter);
output.data = loaded ?? {};
return output;
}
// src/utils/import-formatter.ts
var import_node_path2 = __toESM(require("path"), 1);
function getImportCode(info) {
const specifier = JSON.stringify(info.specifier);
if (info.type === "default") return `import ${info.name} from ${specifier}`;
if (info.type === "namespace")
return `import * as ${info.name} from ${specifier}`;
if (info.type === "named") {
const names = info.names.map(
(name) => Array.isArray(name) ? `${name[0]} as ${name[1]}` : name
);
return `import { ${names.join(", ")} } from ${specifier}`;
}
return `import ${specifier}`;
}
function toImportPath(file, config) {
const ext = import_node_path2.default.extname(file);
const filename = ext === ".ts" ? file.substring(0, file.length - ext.length) : file;
let importPath;
if ("relativeTo" in config) {
importPath = import_node_path2.default.relative(config.relativeTo, filename);
if (!import_node_path2.default.isAbsolute(importPath) && !importPath.startsWith(".")) {
importPath = `./${importPath}`;
}
} else {
importPath = import_node_path2.default.resolve(filename);
}
return importPath.replaceAll(import_node_path2.default.sep, "/");
}
// src/utils/collections.ts
function getSupportedFormats(collection) {
return {
doc: ["mdx", "md"],
meta: ["json", "yaml"]
}[collection.type];
}
function getGlobPatterns(collection) {
if (collection.files) return collection.files;
return [`**/*.{${getSupportedFormats(collection).join(",")}}`];
}
function isFileSupported(filePath, collection) {
for (const format of getSupportedFormats(collection)) {
if (filePath.endsWith(`.${format}`)) return true;
}
return false;
}
// src/map/generate.ts
async function readFileWithCache(file) {
const cached = fileCache.read("read-file", file);
if (cached) return cached;
return (await fs2.readFile(file)).toString();
}
async function generateJS(configPath, config, importPath, configHash = false) {
let asyncInit = false;
const lines = [
getImportCode({
type: "named",
names: ["_runtime"],
specifier: "fumadocs-mdx"
}),
getImportCode({
type: "namespace",
specifier: toImportPath(configPath, importPath),
name: "_source"
})
];
const entries = Array.from(config.collections.entries());
async function getDocEntries(collectionName, files) {
const items = files.map(async (file, i) => {
const importId = `${collectionName}_${i}`;
const params = [`collection=${collectionName}`];
if (configHash) {
params.push(`hash=${configHash}`);
}
lines.unshift(
getImportCode({
type: "namespace",
name: importId,
specifier: `${toImportPath(file.absolutePath, importPath)}?${params.join("&")}`
})
);
return `{ info: ${JSON.stringify(file)}, data: ${importId} }`;
});
return Promise.all(items);
}
async function getMetaEntries(collection, files) {
const items = files.map(async (file) => {
const source = await readFileWithCache(file.absolutePath).catch(() => "");
let data = source.length === 0 ? {} : parseMetaEntry(file.absolutePath, source);
if (collection?.schema) {
data = await validate(
collection.schema,
data,
{
source,
path: file.absolutePath
},
`invalid data in ${file.absolutePath}`
);
}
return JSON.stringify({
info: file,
data
});
});
return Promise.all(items);
}
async function getAsyncEntries(collection, files) {
if (!asyncInit) {
lines.unshift(
getImportCode({
type: "named",
specifier: "fumadocs-mdx/runtime/async",
names: ["_runtimeAsync", "buildConfig"]
}),
"const _sourceConfig = buildConfig(_source)"
);
asyncInit = true;
}
const entries2 = files.map(async (file) => {
const parsed = fumaMatter(
await readFileWithCache(file.absolutePath).catch(() => "")
);
let data = parsed.data;
if (collection.schema) {
data = await validate(
collection.schema,
parsed.data,
{ path: file.absolutePath, source: parsed.content },
`invalid frontmatter in ${file.absolutePath}`
);
}
let lastModified;
if (config.global?.lastModifiedTime === "git") {
lastModified = await getGitTimestamp(file.absolutePath);
}
return JSON.stringify({
info: file,
lastModified,
data,
content: parsed.content
});
});
return Promise.all(entries2);
}
const declares = entries.map(async ([k, collection]) => {
if (collection.type === "docs") {
const docs = await getCollectionFiles(collection.docs);
const metas = await getCollectionFiles(collection.meta);
const metaEntries = (await getMetaEntries(collection.meta, metas)).join(
", "
);
if (collection.docs.async) {
const docsEntries2 = (await getAsyncEntries(collection.docs, docs)).join(
", "
);
return `export const ${k} = _runtimeAsync.docs<typeof _source.${k}>([${docsEntries2}], [${metaEntries}], "${k}", _sourceConfig)`;
}
const docsEntries = (await getDocEntries(k, docs)).join(", ");
return `export const ${k} = _runtime.docs<typeof _source.${k}>([${docsEntries}], [${metaEntries}])`;
}
const files = await getCollectionFiles(collection);
if (collection.type === "doc" && collection.async) {
return `export const ${k} = _runtimeAsync.doc<typeof _source.${k}>([${(await getAsyncEntries(collection, files)).join(", ")}], "${k}", _sourceConfig)`;
}
return `export const ${k} = _runtime.${collection.type}<typeof _source.${k}>([${(await getDocEntries(k, files)).join(", ")}]);`;
});
const resolvedDeclares = await Promise.all(declares);
return [
`// @ts-nocheck -- skip type checking`,
...lines,
...resolvedDeclares
].join("\n");
}
async function getCollectionFiles(collection) {
const files = /* @__PURE__ */ new Map();
const dirs = Array.isArray(collection.dir) ? collection.dir : [collection.dir];
const patterns = getGlobPatterns(collection);
await Promise.all(
dirs.map(async (dir) => {
const result = await (0, import_tinyglobby.glob)(patterns, {
cwd: path4.resolve(dir),
absolute: true
});
for (const item of result) {
if (!isFileSupported(item, collection)) continue;
files.set(item, {
path: path4.relative(dir, item),
absolutePath: item
});
}
})
);
return Array.from(files.values());
}
function parseMetaEntry(file, content) {
const extname2 = path4.extname(file);
try {
if (extname2 === ".json") return JSON.parse(content);
if (extname2 === ".yaml") return (0, import_js_yaml2.load)(content);
} catch (e) {
throw new Error(`Failed to parse meta file: ${file}.`, {
cause: e
});
}
throw new Error(`Unknown meta file format: ${extname2}, in ${file}.`);
}
// src/map/index.ts
async function start(dev, configPath, outDir) {
let configHash = await getConfigHash(configPath);
let config = await loadConfig(configPath, outDir, configHash, true);
const outPath = path5.resolve(outDir, `index.ts`);
async function updateMapFile() {
const start2 = performance.now();
try {
await fs3.writeFile(
outPath,
await generateJS(
configPath,
config,
{ relativeTo: outDir },
configHash
)
);
} catch (err) {
if (err instanceof ValidationError) {
console.error(err.toStringFormatted());
} else {
console.error(err);
}
}
console.log(`[MDX] updated map file in ${performance.now() - start2}ms`);
}
await updateMapFile();
if (dev) {
const { watcher: watcher2 } = await Promise.resolve().then(() => (init_watcher(), watcher_exports));
const instance = watcher2(configPath, config, [outPath]);
instance.on("ready", () => {
console.log("[MDX] started dev server");
});
instance.on("all", (event, file) => {
if (typeof file !== "string") return;
const absolutePath = path5.resolve(file);
const onUpdate = async () => {
const isConfigFile = absolutePath === configPath;
if (isConfigFile) {
configHash = await getConfigHash(configPath);
config = await loadConfig(configPath, outDir, configHash, true);
}
if (event === "change") fileCache.removeCache(absolutePath);
await updateMapFile();
};
void onUpdate();
});
process.on("exit", () => {
console.log("[MDX] closing dev server");
void instance.close();
});
}
}
// src/next/create.ts
var import_node_fs = require("fs");
var defaultPageExtensions = ["mdx", "md", "jsx", "js", "tsx", "ts"];
var isTurboExperimental;
try {
const content = (0, import_node_fs.readFileSync)("./node_modules/next/package.json").toString();
const version = JSON.parse(content).version;
isTurboExperimental = version.startsWith("15.0.") || version.startsWith("15.1.") || version.startsWith("15.2.");
} catch {
isTurboExperimental = false;
}
function createMDX({
configPath = findConfigFile(),
outDir = ".source"
} = {}) {
const isDev = process.argv.includes("dev");
const isBuild = process.argv.includes("build");
if ((isDev || isBuild) && process.env._FUMADOCS_MDX !== "1") {
process.env._FUMADOCS_MDX = "1";
void start(isDev, configPath, outDir);
}
return (nextConfig = {}) => {
const mdxLoaderOptions = {
configPath,
outDir
};
const turbo = {
...nextConfig.experimental?.turbo,
...nextConfig.turbopack,
rules: {
...nextConfig.experimental?.turbo?.rules,
...nextConfig.turbopack?.rules,
"*.{md,mdx}": {
loaders: [
{
loader: "fumadocs-mdx/loader-mdx",
options: mdxLoaderOptions
}
],
as: "*.js"
}
}
};
const updated = {
...nextConfig,
pageExtensions: nextConfig.pageExtensions ?? defaultPageExtensions,
webpack: (config, options) => {
config.resolve ||= {};
config.module ||= {};
config.module.rules ||= [];
config.module.rules.push({
test: /\.mdx?$/,
use: [
options.defaultLoaders.babel,
{
loader: "fumadocs-mdx/loader-mdx",
options: mdxLoaderOptions
}
]
});
config.plugins ||= [];
return nextConfig.webpack?.(config, options) ?? config;
}
};
if (isTurboExperimental) {
updated.experimental = { ...updated.experimental, turbo };
} else {
updated.turbopack = turbo;
}
return updated;
};
}
// src/postinstall.ts
var path6 = __toESM(require("path"), 1);
var fs4 = __toESM(require("fs/promises"), 1);
async function postInstall(configPath = findConfigFile(), outDir = ".source") {
const jsOut = path6.resolve(outDir, "index.ts");
const hash = await getConfigHash(configPath);
const config = await loadConfig(configPath, outDir, hash, true);
await fs4.rm(path6.dirname(jsOut), { recursive: true });
await fs4.mkdir(path6.dirname(jsOut), { recursive: true });
await fs4.writeFile(
jsOut,
await generateJS(configPath, config, { relativeTo: outDir }, hash)
);
console.log("[MDX] types generated");
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
createMDX,
postInstall,
start
});