UNPKG

fumadocs-mdx

Version:

The built-in source for Fumadocs

952 lines (939 loc) 31 kB
//#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;