fumadocs-mdx
Version:
The built-in source for Fumadocs
952 lines (939 loc) • 31 kB
JavaScript
//#region rolldown:runtime
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 __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) {
__defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
}
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
let node_path = require("node:path");
node_path = __toESM(node_path);
let node_url = require("node:url");
let picomatch = require("picomatch");
picomatch = __toESM(picomatch);
let node_fs_promises = require("node:fs/promises");
node_fs_promises = __toESM(node_fs_promises);
let tinyglobby = require("tinyglobby");
let path = require("path");
path = __toESM(path);
let crypto = require("crypto");
let js_yaml = require("js-yaml");
//#region src/config/preset.ts
function pluginOption(def, options = []) {
const list = def(Array.isArray(options) ? options : []).filter(Boolean);
if (typeof options === "function") return options(list);
return list;
}
/**
* apply MDX processor presets
*/
function applyMdxPreset(options = {}) {
return async (environment = "bundler") => {
if (options.preset === "minimal") return options;
const plugins = await import("fumadocs-core/mdx-plugins");
const { valueToExport = [], rehypeCodeOptions, remarkImageOptions, remarkHeadingOptions, remarkStructureOptions, remarkCodeTabOptions, remarkNpmOptions, ...mdxOptions } = options;
const remarkPlugins = pluginOption((v) => [
plugins.remarkGfm,
[plugins.remarkHeading, {
generateToc: false,
...remarkHeadingOptions
}],
remarkImageOptions !== false && [plugins.remarkImage, {
...remarkImageOptions,
useImport: remarkImageOptions?.useImport ?? environment === "bundler"
}],
"remarkCodeTab" in plugins && remarkCodeTabOptions !== false && [plugins.remarkCodeTab, remarkCodeTabOptions],
"remarkNpm" in plugins && remarkNpmOptions !== false && [plugins.remarkNpm, remarkNpmOptions],
...v,
remarkStructureOptions !== false && [plugins.remarkStructure, {
exportAs: "structuredData",
...remarkStructureOptions
}],
valueToExport.length > 0 && (() => {
return (_, file) => {
file.data["mdx-export"] ??= [];
for (const name of valueToExport) {
if (!(name in file.data)) continue;
file.data["mdx-export"].push({
name,
value: file.data[name]
});
}
};
})
], mdxOptions.remarkPlugins);
const rehypePlugins = pluginOption((v) => [
rehypeCodeOptions !== false && [plugins.rehypeCode, rehypeCodeOptions],
...v,
plugins.rehypeToc
], mdxOptions.rehypePlugins);
return {
...mdxOptions,
outputFormat: environment === "runtime" ? "function-body" : mdxOptions.outputFormat,
remarkPlugins,
rehypePlugins
};
};
}
//#endregion
//#region src/config/build.ts
const SupportedFormats = {
doc: ["mdx", "md"],
meta: ["json", "yaml"]
};
function buildCollection(name, collection, cwd) {
if (collection.type === "docs") return {
...collection,
type: "docs",
get dir() {
return this.docs.dir;
},
name,
meta: buildCollection(name, collection.meta, cwd),
docs: buildCollection(name, collection.docs, cwd),
hasFile(filePath) {
return this.docs.hasFile(filePath) || this.meta.hasFile(filePath);
},
isFileSupported(filePath) {
return this.docs.isFileSupported(filePath) || this.meta.isFileSupported(filePath);
},
cwd
};
return {
...collection,
...buildPrimitiveCollection(name, collection, cwd)
};
}
function buildPrimitiveCollection(name, config, cwd) {
const supportedFormats = SupportedFormats[config.type];
const patterns = config.files ?? [`**/*.{${supportedFormats.join(",")}}`];
let matcher;
return {
dir: node_path.default.resolve(cwd, config.dir),
cwd,
name,
patterns,
isFileSupported(filePath) {
return supportedFormats.some((format) => filePath.endsWith(`.${format}`));
},
hasFile(filePath) {
if (!this.isFileSupported(filePath)) return false;
const relativePath = node_path.default.relative(this.dir, filePath);
if (relativePath.startsWith(`..${node_path.default.sep}`)) return false;
return (matcher ??= (0, picomatch.default)(patterns))(relativePath);
}
};
}
function buildConfig(config, cwd = process.cwd()) {
const collections = /* @__PURE__ */ new Map();
const loaded = {};
for (const [k, v] of Object.entries(config)) {
if (!v) continue;
if (typeof v === "object" && "type" in v) {
if (v.type === "docs" || v.type === "doc" || v.type === "meta") {
collections.set(k, buildCollection(k, v, cwd));
continue;
}
}
if (k === "default" && v) {
Object.assign(loaded, v);
continue;
}
throw new Error(`Unknown export "${k}", you can only export collections from source configuration file.`);
}
const mdxOptionsCache = /* @__PURE__ */ new Map();
return {
global: loaded,
collections,
workspaces: Object.fromEntries(Object.entries(loaded.workspaces ?? {}).map(([key, value]) => {
return [key, {
dir: value.dir,
config: buildConfig(value.config, node_path.default.resolve(cwd, value.dir))
}];
})),
getMDXOptions(collection, environment = "bundler") {
const key = collection ? `${environment}:${collection.name}` : environment;
const cached = mdxOptionsCache.get(key);
if (cached) return cached;
let result;
if (collection?.mdxOptions) {
const optionsFn = collection.mdxOptions;
result = typeof optionsFn === "function" ? optionsFn(environment) : optionsFn;
} else result = (async () => {
const optionsFn = this.global.mdxOptions;
return applyMdxPreset(typeof optionsFn === "function" ? await optionsFn() : optionsFn)(environment);
})();
mdxOptionsCache.set(key, result);
return result;
}
};
}
//#endregion
//#region src/config/load-from-file.ts
async function compileConfig(core) {
const { build } = await import("esbuild");
const { configPath, outDir } = core.getOptions();
if ((await build({
entryPoints: [{
in: configPath,
out: "source.config"
}],
bundle: true,
outdir: outDir,
target: "node20",
write: true,
platform: "node",
format: "esm",
packages: "external",
outExtension: { ".js": ".mjs" },
allowOverwrite: true
})).errors.length > 0) throw new Error("failed to compile configuration file");
}
/**
* Load config
*
* @param build - By default, it assumes the config file has been compiled. Set this `true` to compile the config first.
*/
async function loadConfig(core, build = false) {
if (build) await compileConfig(core);
const url = (0, node_url.pathToFileURL)(core.getCompiledConfigPath());
url.searchParams.set("hash", Date.now().toString());
return await import(url.href).then((loaded) => buildConfig(loaded));
}
//#endregion
//#region src/utils/validation.ts
var ValidationError = class extends Error {
constructor(message, issues) {
super(`${message}:\n${issues.map((issue) => ` ${issue.path}: ${issue.message}`).join("\n")}`);
this.title = message;
this.issues = issues;
}
async toStringFormatted() {
const picocolorsModule = await import("picocolors");
const picocolors = picocolorsModule.default ?? picocolorsModule;
return [picocolors.bold(`[MDX] ${this.title}:`), ...this.issues.map((issue) => picocolors.redBright(`- ${picocolors.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;
}
//#endregion
//#region src/utils/codegen.ts
/**
* Code generator (one instance per file)
*/
function createCodegen({ target = "default", outDir = "", jsExtension = false, globCache = /* @__PURE__ */ new Map() }) {
let eagerImportId = 0;
const banner = ["// @ts-nocheck"];
if (target === "vite") banner.push("/// <reference types=\"vite/client\" />");
return {
options: {
target,
outDir
},
lines: [],
addImport(statement) {
this.lines.unshift(statement);
},
async pushAsync(insert) {
for (const line of await Promise.all(insert)) {
if (line === void 0) continue;
this.lines.push(line);
}
},
async generateGlobImport(patterns, options) {
if (target === "vite") return this.generateViteGlobImport(patterns, options);
return this.generateNodeGlobImport(patterns, options);
},
generateViteGlobImport(patterns, { base, ...rest }) {
patterns = (typeof patterns === "string" ? [patterns] : patterns).map(normalizeViteGlobPath);
return `import.meta.glob(${JSON.stringify(patterns)}, ${JSON.stringify({
base: normalizeViteGlobPath(node_path.default.relative(outDir, base)),
...rest
}, null, 2)})`;
},
async generateNodeGlobImport(patterns, { base, eager = false, query = {}, import: importName }) {
const cacheKey = JSON.stringify({
patterns,
base
});
let files = globCache.get(cacheKey);
if (!files) {
files = (0, tinyglobby.glob)(patterns, { cwd: base });
globCache.set(cacheKey, files);
}
let code = "{";
for (const item of await files) {
const fullPath = node_path.default.join(base, item);
const searchParams = new URLSearchParams();
for (const [k, v] of Object.entries(query)) if (v !== void 0) searchParams.set(k, v);
const importPath = this.formatImportPath(fullPath) + "?" + searchParams.toString();
if (eager) {
const name = `__fd_glob_${eagerImportId++}`;
this.lines.unshift(importName ? `import { ${importName} as ${name} } from ${JSON.stringify(importPath)}` : `import * as ${name} from ${JSON.stringify(importPath)}`);
code += `${JSON.stringify(item)}: ${name}, `;
} else {
let line = `${JSON.stringify(item)}: () => import(${JSON.stringify(importPath)})`;
if (importName) line += `.then(mod => mod.${importName})`;
code += `${line}, `;
}
}
code += "}";
return code;
},
formatImportPath(file) {
const ext = node_path.default.extname(file);
let filename;
if (ext === ".ts") {
filename = file.substring(0, file.length - ext.length);
if (jsExtension) filename += ".js";
} else filename = file;
const importPath = slash(node_path.default.relative(outDir, filename));
return importPath.startsWith(".") ? importPath : `./${importPath}`;
},
toString() {
return [...banner, ...this.lines].join("\n");
}
};
}
/**
* convert into POSIX & relative file paths, such that Vite can accept it.
*/
function normalizeViteGlobPath(file) {
file = slash(file);
if (file.startsWith("./")) return file;
if (file.startsWith("/")) return `.${file}`;
return `./${file}`;
}
function slash(path$6) {
if (path$6.startsWith("\\\\?\\")) return path$6;
return path$6.replaceAll("\\", "/");
}
function ident(code, tab = 1) {
return code.split("\n").map((v) => " ".repeat(tab) + v).join("\n");
}
//#endregion
//#region src/core.ts
const _Defaults = {
configPath: "source.config.ts",
outDir: ".source"
};
async function getPlugins(pluginOptions) {
const plugins = [];
for await (const option of pluginOptions) {
if (!option) continue;
if (Array.isArray(option)) plugins.push(...await getPlugins(option));
else plugins.push(option);
}
return plugins;
}
function createCore(options) {
let config;
let plugins;
const workspaces = /* @__PURE__ */ new Map();
async function transformMetadata({ collection, filePath, source }, data) {
if (collection.schema) data = await validate(collection.schema, data, {
path: filePath,
source
}, collection.type === "doc" ? `invalid frontmatter in ${filePath}` : `invalid data in ${filePath}`);
return data;
}
return {
cache: /* @__PURE__ */ new Map(),
async init({ config: newConfig }) {
config = await newConfig;
this.cache.clear();
workspaces.clear();
plugins = await getPlugins([
postprocessPlugin(),
options.plugins,
config.global.plugins
]);
for (const plugin of plugins) {
const out = await plugin.config?.call(this.getPluginContext(), config);
if (out) config = out;
}
if (!options.workspace) await Promise.all(Object.entries(config.workspaces).map(async ([name, workspace]) => {
const core = createCore({
...options,
outDir: node_path.default.join(options.outDir, name),
workspace: {
name,
parent: this,
dir: workspace.dir
}
});
await core.init({ config: workspace.config });
workspaces.set(name, core);
}));
},
getWorkspaces() {
return workspaces;
},
getOptions() {
return options;
},
getConfig() {
return config;
},
getCompiledConfigPath() {
return node_path.default.join(options.outDir, "source.config.mjs");
},
getPlugins() {
return plugins;
},
getCollections() {
return Array.from(config.collections.values());
},
getCollection(name) {
return config.collections.get(name);
},
getPluginContext() {
return { core: this };
},
async initServer(server) {
const ctx = this.getPluginContext();
for (const plugin of plugins) await plugin.configureServer?.call(ctx, server);
for (const workspace of workspaces.values()) await workspace.initServer(server);
},
async emit(emitOptions = {}) {
const { filterPlugin, filterWorkspace, write = false } = emitOptions;
const start = performance.now();
const ctx = this.getPluginContext();
const added = /* @__PURE__ */ new Set();
const out = {
entries: [],
workspaces: {}
};
for (const li of await Promise.all(plugins.map((plugin) => {
if (filterPlugin && !filterPlugin(plugin) || !plugin.emit) return;
return plugin.emit.call(ctx);
}))) {
if (!li) continue;
for (const item of li) {
if (added.has(item.path)) continue;
out.entries.push(item);
added.add(item.path);
}
}
if (write) {
await Promise.all(out.entries.map(async (entry) => {
const file = node_path.default.join(options.outDir, entry.path);
await node_fs_promises.default.mkdir(node_path.default.dirname(file), { recursive: true });
await node_fs_promises.default.writeFile(file, entry.content);
}));
console.log(options.workspace ? `[MDX: ${options.workspace.name}] generated files in ${performance.now() - start}ms` : `[MDX] generated files in ${performance.now() - start}ms`);
}
for (const [name, workspace] of workspaces) {
if (filterWorkspace && !filterWorkspace(name)) continue;
out.workspaces[name] = (await workspace.emit(emitOptions)).entries;
}
return out;
},
async transformMeta(options$1, data) {
const ctx = {
...this.getPluginContext(),
...options$1
};
data = await transformMetadata(options$1, data);
for (const plugin of plugins) if (plugin.meta?.transform) data = await plugin.meta.transform.call(ctx, data) ?? data;
return data;
},
async transformFrontmatter(options$1, data) {
const ctx = {
...this.getPluginContext(),
...options$1
};
data = await transformMetadata(options$1, data);
for (const plugin of plugins) if (plugin.doc?.frontmatter) data = await plugin.doc.frontmatter.call(ctx, data) ?? data;
return data;
},
async transformVFile(options$1, file) {
const ctx = {
...this.getPluginContext(),
...options$1
};
for (const plugin of plugins) if (plugin.doc?.vfile) file = await plugin.doc.vfile.call(ctx, file) ?? file;
return file;
}
};
}
function postprocessPlugin() {
const LinkReferenceTypes = `{
/**
* extracted references (e.g. hrefs, paths), useful for analyzing relationships between pages.
*/
extractedReferences: import("fumadocs-mdx").ExtractedReference[];
}`;
return { "index-file": {
generateTypeConfig() {
const lines = [];
lines.push("{");
lines.push(" DocData: {");
for (const collection of this.core.getCollections()) {
let postprocessOptions;
switch (collection.type) {
case "doc":
postprocessOptions = collection.postprocess;
break;
case "docs":
postprocessOptions = collection.docs.postprocess;
break;
}
if (postprocessOptions?.extractLinkReferences) lines.push(ident(`${collection.name}: ${LinkReferenceTypes},`, 2));
}
lines.push(" }");
lines.push("}");
return lines.join("\n");
},
serverOptions(options) {
options.doc ??= {};
options.doc.passthroughs ??= [];
options.doc.passthroughs.push("extractedReferences");
}
} };
}
//#endregion
//#region src/loaders/index.ts
const metaLoaderGlob = /\.(json|yaml)(\?.+?)?$/;
const mdxLoaderGlob = /\.mdx?(\?.+?)?$/;
//#endregion
//#region src/utils/fs-cache.ts
const map = /* @__PURE__ */ new Map();
function createFSCache() {
return {
read(file) {
const fullPath = toFullPath(file);
const cached = map.get(fullPath);
if (cached) return cached;
const read = node_fs_promises.default.readFile(fullPath).then((s) => s.toString());
map.set(fullPath, read);
return read;
},
delete(file) {
map.delete(toFullPath(file));
}
};
}
/**
* make file paths relative to cwd
*/
function toFullPath(file) {
if (node_path.default.isAbsolute(file)) return node_path.default.relative(process.cwd(), file);
return file;
}
//#endregion
//#region src/utils/fuma-matter.ts
/**
* Inspired by https://github.com/jonschlinkert/gray-matter
*/
const regex = /^---\r?\n(.+?)\r?\n---\r?\n?/s;
/**
* parse frontmatter, it supports only yaml format
*/
function fumaMatter(input) {
const output = {
matter: "",
data: {},
content: input
};
const match = regex.exec(input);
if (!match) return output;
output.matter = match[0];
output.content = input.slice(match[0].length);
output.data = (0, js_yaml.load)(match[1]) ?? {};
return output;
}
//#endregion
//#region src/plugins/index-file.ts
const indexFileCache = createFSCache();
function indexFile(options = {}) {
const { target = "default", addJsExtension, browser = true, dynamic = true } = options;
let dynamicCollections;
function isDynamic(collection) {
return collection.type === "docs" && collection.docs.dynamic || collection.type === "doc" && collection.dynamic;
}
function generateConfigs(core) {
const serverOptions = {};
const typeConfigs = ["import(\"fumadocs-mdx/runtime/types\").InternalTypeConfig"];
const ctx = core.getPluginContext();
for (const plugin of core.getPlugins()) {
const indexFilePlugin = plugin["index-file"];
if (!indexFilePlugin) continue;
indexFilePlugin.serverOptions?.call(ctx, serverOptions);
const config = indexFilePlugin.generateTypeConfig?.call(ctx);
if (config) typeConfigs.push(config);
}
return {
serverOptions,
tc: typeConfigs.join(" & ")
};
}
return {
name: "index-file",
config() {
dynamicCollections = this.core.getCollections().filter(isDynamic);
},
configureServer(server) {
if (!server.watcher) return;
server.watcher.on("all", async (event, file) => {
indexFileCache.delete(file);
if (dynamicCollections.length === 0) {
if (target === "vite") return;
if (target === "default" && event === "change") return;
}
const updatedCollection = this.core.getCollections().find((collection) => collection.hasFile(file));
if (!updatedCollection) return;
if (!isDynamic(updatedCollection)) {
if (target === "vite") return;
if (target === "default" && event === "change") return;
}
await this.core.emit({
filterPlugin: (plugin) => plugin.name === "index-file",
filterWorkspace: () => false,
write: true
});
});
},
async emit() {
const globCache = /* @__PURE__ */ new Map();
const { workspace, outDir } = this.core.getOptions();
const { serverOptions, tc } = generateConfigs(this.core);
const toEmitEntry = async (path$6, content) => {
const codegen = createCodegen({
target,
outDir,
jsExtension: addJsExtension,
globCache
});
await content({
core: this.core,
codegen,
serverOptions,
tc,
workspace: workspace?.name
});
return {
path: path$6,
content: codegen.toString()
};
};
const out = [toEmitEntry("server.ts", generateServerIndexFile)];
if (dynamic) out.push(toEmitEntry("dynamic.ts", generateDynamicIndexFile));
if (browser) out.push(toEmitEntry("browser.ts", generateBrowserIndexFile));
return await Promise.all(out);
}
};
}
async function generateServerIndexFile(ctx) {
const { core, codegen, serverOptions, tc } = ctx;
codegen.lines.push(`import { server } from 'fumadocs-mdx/runtime/server';`, `import type * as Config from '${codegen.formatImportPath(core.getOptions().configPath)}';`, "", `const create = server<typeof Config, ${tc}>(${JSON.stringify(serverOptions)});`);
async function generateCollectionObject(collection) {
const base = getBase(collection);
switch (collection.type) {
case "docs": {
if (collection.docs.dynamic) return;
if (collection.docs.async) {
const [metaGlob$1, headGlob, bodyGlob] = await Promise.all([
generateMetaCollectionGlob(ctx, collection.meta, true),
generateDocCollectionFrontmatterGlob(ctx, collection.docs, true),
generateDocCollectionGlob(ctx, collection.docs)
]);
return `await create.docsLazy("${collection.name}", "${base}", ${metaGlob$1}, ${headGlob}, ${bodyGlob})`;
}
const [metaGlob, docGlob] = await Promise.all([generateMetaCollectionGlob(ctx, collection.meta, true), generateDocCollectionGlob(ctx, collection.docs, true)]);
return `await create.docs("${collection.name}", "${base}", ${metaGlob}, ${docGlob})`;
}
case "doc":
if (collection.dynamic) return;
if (collection.async) {
const [headGlob, bodyGlob] = await Promise.all([generateDocCollectionFrontmatterGlob(ctx, collection, true), generateDocCollectionGlob(ctx, collection)]);
return `await create.docLazy("${collection.name}", "${base}", ${headGlob}, ${bodyGlob})`;
}
return `await create.doc("${collection.name}", "${base}", ${await generateDocCollectionGlob(ctx, collection, true)})`;
case "meta": return `await create.meta("${collection.name}", "${base}", ${await generateMetaCollectionGlob(ctx, collection, true)})`;
}
}
await codegen.pushAsync(core.getCollections().map(async (collection) => {
const obj = await generateCollectionObject(collection);
if (!obj) return;
return `\nexport const ${collection.name} = ${obj};`;
}));
}
async function generateDynamicIndexFile(ctx) {
const { core, codegen, serverOptions, tc } = ctx;
const { configPath, environment, outDir } = core.getOptions();
const partialOptions = {
configPath,
environment,
outDir
};
codegen.lines.push(`import { dynamic } from 'fumadocs-mdx/runtime/dynamic';`, `import * as Config from '${codegen.formatImportPath(configPath)}';`, "", `const create = await dynamic<typeof Config, ${tc}>(Config, ${JSON.stringify(partialOptions)}, ${JSON.stringify(serverOptions)});`);
async function generateCollectionObjectEntry(collection, absolutePath) {
const fullPath = path.default.relative(process.cwd(), absolutePath);
const content = await indexFileCache.read(fullPath).catch(() => "");
const parsed = fumaMatter(content);
const data = await core.transformFrontmatter({
collection,
filePath: fullPath,
source: content
}, parsed.data);
const hash = (0, crypto.createHash)("md5").update(content).digest("hex");
const infoStr = [`absolutePath: path.resolve(${JSON.stringify(fullPath)})`];
for (const [k, v] of Object.entries({
info: {
fullPath,
path: path.default.relative(collection.dir, absolutePath)
},
data,
hash
})) infoStr.push(`${k}: ${JSON.stringify(v)}`);
return `{ ${infoStr.join(", ")} }`;
}
async function generateCollectionObject(parent) {
let collection;
if (parent.type === "doc") collection = parent;
else if (parent.type === "docs") collection = parent.docs;
if (!collection || !collection.dynamic) return;
const files = await (0, tinyglobby.glob)(collection.patterns, {
cwd: collection.dir,
absolute: true
});
const entries = await Promise.all(files.map((file) => generateCollectionObjectEntry(collection, file)));
switch (parent.type) {
case "docs": {
const metaGlob = await generateMetaCollectionGlob(ctx, parent.meta, true);
return `await create.docs("${parent.name}", "${getBase(parent)}", ${metaGlob}, ${entries.join(", ")})`;
}
case "doc": return `await create.doc("${collection.name}", "${getBase(collection)}", ${entries.join(", ")})`;
}
}
await codegen.pushAsync(core.getCollections().map(async (collection) => {
const obj = await generateCollectionObject(collection);
if (!obj) return;
return `\nexport const ${collection.name} = ${obj};`;
}));
}
async function generateBrowserIndexFile(ctx) {
const { core, codegen, tc } = ctx;
codegen.lines.push(`import { browser } from 'fumadocs-mdx/runtime/browser';`, `import type * as Config from '${codegen.formatImportPath(core.getOptions().configPath)}';`, "", `const create = browser<typeof Config, ${tc}>();`);
async function generateCollectionObject(collection) {
switch (collection.type) {
case "docs":
if (collection.docs.dynamic) return;
return generateCollectionObject(collection.docs);
case "doc":
if (collection.dynamic) return;
return `create.doc("${collection.name}", ${await generateDocCollectionGlob(ctx, collection)})`;
}
}
codegen.lines.push("const browserCollections = {");
await codegen.pushAsync(core.getCollections().map(async (collection) => {
const obj = await generateCollectionObject(collection);
if (!obj) return;
return ident(`${collection.name}: ${obj},`);
}));
codegen.lines.push("};", "export default browserCollections;");
}
function getBase(collection) {
return slash(path.default.relative(process.cwd(), collection.dir));
}
function generateDocCollectionFrontmatterGlob({ codegen, workspace }, collection, eager = false) {
return codegen.generateGlobImport(collection.patterns, {
query: {
collection: collection.name,
only: "frontmatter",
workspace
},
import: "frontmatter",
base: collection.dir,
eager
});
}
function generateDocCollectionGlob({ codegen, workspace }, collection, eager = false) {
return codegen.generateGlobImport(collection.patterns, {
query: {
collection: collection.name,
workspace
},
base: collection.dir,
eager
});
}
function generateMetaCollectionGlob({ codegen, workspace }, collection, eager = false) {
return codegen.generateGlobImport(collection.patterns, {
query: {
collection: collection.name,
workspace
},
import: "default",
base: collection.dir,
eager
});
}
//#endregion
//#region src/next/index.ts
const defaultPageExtensions = [
"mdx",
"md",
"jsx",
"js",
"tsx",
"ts"
];
function createMDX(createOptions = {}) {
const core = createNextCore(applyDefaults(createOptions));
const isDev = process.env.NODE_ENV === "development";
if (process.env._FUMADOCS_MDX !== "1") {
process.env._FUMADOCS_MDX = "1";
init(isDev, core);
}
return (nextConfig = {}) => {
const { configPath, outDir } = core.getOptions();
const loaderOptions = {
configPath,
outDir,
absoluteCompiledConfigPath: node_path.resolve(core.getCompiledConfigPath()),
isDev
};
const turbopack = {
...nextConfig.turbopack,
rules: {
...nextConfig.turbopack?.rules,
"*.{md,mdx}": {
loaders: [{
loader: "fumadocs-mdx/loader-mdx",
options: loaderOptions
}],
as: "*.js"
},
"*.json": {
loaders: [{
loader: "fumadocs-mdx/loader-meta",
options: loaderOptions
}],
as: "*.json"
},
"*.yaml": {
loaders: [{
loader: "fumadocs-mdx/loader-meta",
options: loaderOptions
}],
as: "*.js"
}
}
};
return {
...nextConfig,
turbopack,
pageExtensions: nextConfig.pageExtensions ?? defaultPageExtensions,
webpack: (config, options) => {
config.resolve ||= {};
config.module ||= {};
config.module.rules ||= [];
config.module.rules.push({
test: mdxLoaderGlob,
use: [options.defaultLoaders.babel, {
loader: "fumadocs-mdx/loader-mdx",
options: loaderOptions
}]
}, {
test: metaLoaderGlob,
enforce: "pre",
use: [{
loader: "fumadocs-mdx/loader-meta",
options: loaderOptions
}]
});
config.plugins ||= [];
return nextConfig.webpack?.(config, options) ?? config;
}
};
};
}
async function init(dev, core) {
async function initOrReload() {
await core.init({ config: loadConfig(core, true) });
await core.emit({ write: true });
}
async function devServer() {
const { FSWatcher } = await import("chokidar");
const { configPath, outDir } = core.getOptions();
const watcher = new FSWatcher({
ignoreInitial: true,
persistent: true,
ignored: [outDir]
});
watcher.add(configPath);
for (const collection of core.getCollections()) watcher.add(collection.dir);
for (const workspace of core.getWorkspaces().values()) for (const collection of workspace.getCollections()) watcher.add(collection.dir);
watcher.on("ready", () => {
console.log("[MDX] started dev server");
});
const absoluteConfigPath = node_path.resolve(configPath);
watcher.on("all", async (_event, file) => {
if (node_path.resolve(file) === absoluteConfigPath) {
watcher.removeAllListeners();
await watcher.close();
await initOrReload();
console.log("[MDX] restarting dev server");
await devServer();
}
});
process.on("exit", () => {
if (watcher.closed) return;
console.log("[MDX] closing dev server");
watcher.close();
});
await core.initServer({ watcher });
}
await initOrReload();
if (dev) await devServer();
}
async function postInstall(options) {
const core = createNextCore(applyDefaults(options));
await core.init({ config: loadConfig(core, true) });
await core.emit({ write: true });
}
function applyDefaults(options) {
return {
index: {},
outDir: options.outDir ?? _Defaults.outDir,
configPath: options.configPath ?? _Defaults.configPath
};
}
function createNextCore(options) {
return createCore({
environment: "next",
outDir: options.outDir,
configPath: options.configPath,
plugins: [options.index && indexFile(options.index)]
});
}
//#endregion
exports.createMDX = createMDX;
exports.postInstall = postInstall;