UNPKG

fumadocs-mdx

Version:

The built-in source for Fumadocs

703 lines (683 loc) 21.1 kB
"use strict"; 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/vite/index.ts var vite_exports = {}; __export(vite_exports, { default: () => mdx }); module.exports = __toCommonJS(vite_exports); // 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/build-mdx.ts var import_mdx = require("@mdx-js/mdx"); // src/mdx-plugins/remark-include.ts var import_unist_util_visit = require("unist-util-visit"); var path = __toESM(require("path"), 1); var fs = __toESM(require("fs/promises"), 1); // 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/mdx-plugins/remark-include.ts function flattenNode(node) { if ("children" in node) return node.children.map((child) => flattenNode(child)).join(""); if ("value" in node) return node.value; return ""; } function parseSpecifier(specifier) { const idx = specifier.lastIndexOf("#"); if (idx === -1) return { file: specifier }; return { file: specifier.slice(0, idx), section: specifier.slice(idx + 1) }; } function extractSection(root, section) { for (const node of root.children) { if (node.type === "mdxJsxFlowElement" && node.name === "section" && node.attributes.some( (attr) => attr.type === "mdxJsxAttribute" && attr.name === "id" && attr.value === section )) { return { type: "root", children: node.children }; } } } function remarkInclude() { const TagName = "include"; async function update(tree, directory, data) { const queue = []; (0, import_unist_util_visit.visit)( tree, ["mdxJsxFlowElement", "mdxJsxTextElement"], (node, _, parent) => { let specifier; const params = {}; if ((node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && node.name === TagName) { const value = flattenNode(node); if (value.length > 0) { for (const attr of node.attributes) { if (attr.type === "mdxJsxAttribute" && (typeof attr.value === "string" || attr.value === null)) { params[attr.name] = attr.value; } } specifier = value; } } if (!specifier) return; const { file, section } = parseSpecifier(specifier); const targetPath = path.resolve( "cwd" in params ? process.cwd() : directory, file ); const asCode = params.lang || !file.endsWith(".md") && !file.endsWith(".mdx"); queue.push( fs.readFile(targetPath).then((buffer) => buffer.toString()).then(async (content) => { data._compiler?.addDependency(targetPath); if (asCode) { const lang = params.lang ?? path.extname(file).slice(1); Object.assign(node, { type: "code", lang, meta: params.meta, value: content, data: {} }); return; } const processor = data._processor ? data._processor.getProcessor( targetPath.endsWith(".md") ? "md" : "mdx" ) : this; let parsed = processor.parse(fumaMatter(content).content); if (section) { const extracted = extractSection(parsed, section); if (!extracted) throw new Error( `Cannot find section ${section} in ${file}, make sure you have encapsulated the section in a <section id="${section}"> tag` ); parsed = extracted; } await update.call( processor, parsed, path.dirname(targetPath), data ); Object.assign( parent && parent.type === "paragraph" ? parent : node, parsed ); }).catch((e) => { throw new Error( `failed to read file ${targetPath} ${e instanceof Error ? e.message : String(e)}`, { cause: e } ); }) ); return "skip"; } ); await Promise.all(queue); } return async (tree, file) => { await update.call(this, tree, path.dirname(file.path), file.data); }; } // src/utils/build-mdx.ts var cache = /* @__PURE__ */ new Map(); async function buildMDX(cacheKey, source, options) { const { filePath, frontmatter, data, _compiler, ...rest } = options; function getProcessor(format) { const key = `${cacheKey}:${format}`; let processor = cache.get(key); if (!processor) { processor = (0, import_mdx.createProcessor)({ outputFormat: "program", ...rest, remarkPlugins: [remarkInclude, ...rest.remarkPlugins ?? []], format }); cache.set(key, processor); } return processor; } return getProcessor( options.format ?? filePath.endsWith(".mdx") ? "mdx" : "md" ).process({ value: source, path: filePath, data: { ...data, frontmatter, _compiler, _processor: { getProcessor } } }); } // src/vite/index.ts var import_node_querystring = require("querystring"); // src/utils/count-lines.ts function countLines(s) { let num = 0; for (const c of s) { if (c === "\n") num++; } return num; } // 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/vite/index.ts var import_zod = require("zod"); // src/utils/import-formatter.ts var import_node_path = __toESM(require("path"), 1); function toImportPath(file, config) { const ext = import_node_path.default.extname(file); const filename = ext === ".ts" ? file.substring(0, file.length - ext.length) : file; let importPath; if ("relativeTo" in config) { importPath = import_node_path.default.relative(config.relativeTo, filename); if (!import_node_path.default.isAbsolute(importPath) && !importPath.startsWith(".")) { importPath = `./${importPath}`; } } else { importPath = import_node_path.default.resolve(filename); } return importPath.replaceAll(import_node_path.default.sep, "/"); } function ident(code, tab = 1) { return code.split("\n").map((v) => " ".repeat(tab) + v).join("\n"); } // src/vite/index.ts var fs2 = __toESM(require("fs/promises"), 1); var path4 = __toESM(require("path"), 1); var import_js_yaml2 = require("js-yaml"); // 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(",")}}`]; } // src/vite/generate-glob.ts function generateGlob(name, collection) { const patterns = mapGlobPatterns(getGlobPatterns(collection)); const options = { 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)})`; } function mapGlobPatterns(patterns) { return patterns.map((file) => { if (file.startsWith("./")) return file; if (file.startsWith("/")) return `.${file}`; return `./${file}`; }); } function getGlobBase(collection) { 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]; } if (!dir.startsWith("./") && !dir.startsWith("/")) { return "/" + dir; } return dir; } // src/utils/git-timestamp.ts var import_node_path2 = __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_path2.default.relative(process.cwd(), file)], { throwOnError: true } ); const time = new Date(out.stdout); cache2.set(file, time); return time; } catch { return; } } // src/vite/index.ts var onlySchema = import_zod.z.literal(["frontmatter", "all"]); function mdx(config, options = {}) { const { generateIndexFile = true, configPath = "source.config.ts" } = options; const loaded = buildConfig(config); async function transformMeta(path5, query, value) { const isJson = path5.endsWith(".json"); const parsed = (0, import_node_querystring.parse)(query); const collection = parsed.collection ? loaded.collections.get(parsed.collection) : void 0; if (!collection) return null; let schema; switch (collection.type) { case "meta": schema = collection.schema; break; case "docs": schema = collection.meta.schema; break; } if (!schema) return null; let data; try { data = isJson ? JSON.parse(value) : (0, import_js_yaml2.load)(value); } catch { return null; } const out = await validate( schema, data, { path: path5, source: value }, `invalid data in ${path5}` ); return { code: isJson ? JSON.stringify(out) : `export default ${JSON.stringify(out)}`, map: null }; } async function transformContent(file, query, value) { const matter = fumaMatter(value); const isDevelopment = this.environment.mode === "dev"; const parsed = (0, import_node_querystring.parse)(query); const collection = parsed.collection ? loaded.collections.get(parsed.collection) : void 0; const only = parsed.only ? onlySchema.parse(parsed.only) : "all"; let schema; let mdxOptions; switch (collection?.type) { case "doc": mdxOptions = collection.mdxOptions; schema = collection.schema; break; case "docs": mdxOptions = collection.docs.mdxOptions; schema = collection.docs.schema; break; } if (schema) { matter.data = await validate( schema, matter.data, { source: value, path: file }, `invalid frontmatter in ${file}` ); } if (only === "frontmatter") { return { code: `export const frontmatter = ${JSON.stringify(matter.data)}`, map: null }; } const data = {}; if (loaded.global.lastModifiedTime === "git") { data.lastModified = (await getGitTimestamp(file))?.getTime(); } mdxOptions ??= await loaded.getDefaultMDXOptions(); const lineOffset = isDevelopment ? countLines(matter.matter) : 0; const compiled = await buildMDX( parsed.collection ?? "global", "\n".repeat(lineOffset) + matter.content, { development: isDevelopment, ...mdxOptions, data, filePath: file, frontmatter: matter.data, _compiler: { addDependency: (file2) => { this.addWatchFile(file2); } } } ); return { code: String(compiled.value), map: compiled.map }; } return { name: "fumadocs-mdx", // needed, otherwise other plugins will be executed before our `transform`. enforce: "pre", async buildStart() { if (!generateIndexFile) return; console.log("[Fumadocs MDX] Generating index files"); const outdir = process.cwd(); const outFile = "source.generated.ts"; const lines = [ '/// <reference types="vite/client" />', `import { fromConfig } from 'fumadocs-mdx/runtime/vite';`, `import type * as Config from '${toImportPath(configPath, { relativeTo: outdir })}';`, "", `export const create = fromConfig<typeof Config>();` ]; function docs(name, collection) { const args = [ ident(`doc: ${generateGlob(name, collection.docs)}`), ident(`meta: ${generateGlob(name, collection.meta)}`) ].join(",\n"); return `export const ${name} = create.docs("${name}", { ${args} });`; } function doc(name, collection) { return `export const ${name} = create.doc("${name}", ${generateGlob(name, collection)});`; } function meta(name, collection) { return `export const ${name} = create.meta("${name}", ${generateGlob(name, collection)});`; } for (const [name, collection] of loaded.collections.entries()) { lines.push(""); if (collection.type === "docs") { lines.push(docs(name, collection)); } else if (collection.type === "meta") { lines.push(meta(name, collection)); } else { lines.push(doc(name, collection)); } } await fs2.writeFile(path4.join(outdir, outFile), lines.join("\n")); }, async transform(value, id) { const [file, query = ""] = id.split("?"); const ext = path4.extname(file); try { if ([".yaml", ".json"].includes(ext)) return await transformMeta(file, query, value); if ([".md", ".mdx"].includes(ext)) return await transformContent.call(this, file, query, value); } catch (e) { if (e instanceof ValidationError) { throw new Error(e.toStringFormatted()); } throw e; } } }; }