fumadocs-docgen
Version:
Useful remark utilities and plugins
204 lines (203 loc) • 5.55 kB
JavaScript
import { visit } from "unist-util-visit";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { z } from "zod";
import convert from "npm-to-yarn";
//#region src/remark-docgen.ts
const metaRegex = /doc-gen:(?<name>.+)/;
function remarkDocGen({ generators = [] }) {
return async (tree, file) => {
generators.forEach((gen) => gen.onFile?.(tree, file));
const queue = [];
visit(tree, "code", (code, _, parent) => {
if (code.lang !== "json" || !code.meta || !parent) return;
const matches = metaRegex.exec(code.meta);
if (!matches) return;
const name = matches[1];
const gen = generators.find((g) => g.name === name);
const run = async () => {
const result = await gen?.run(JSON.parse(code.value), {
cwd: file.cwd,
path: file.path,
node: code
});
const index = parent.children.findIndex((c) => c === code);
if (result && index !== -1) {
const items = Array.isArray(result) ? result : [result];
parent.children.splice(index, 1, ...items);
}
};
queue.push(run());
});
await Promise.all(queue);
};
}
//#endregion
//#region src/utils.ts
function createElement(name, attributes, children) {
const element = {
type: "mdxJsxFlowElement",
name,
attributes
};
if (children) element.children = children;
return element;
}
function expressionToAttribute(key, value) {
return {
type: "mdxJsxAttribute",
name: key,
value: {
type: "mdxJsxAttributeValueExpression",
value: "",
data: { estree: {
type: "Program",
body: [{
type: "ExpressionStatement",
expression: value
}]
} }
}
};
}
//#endregion
//#region src/file-generator.ts
const fileGeneratorSchema = z.object({
file: z.string(),
/**
* Turn file content into a code block
*
* @defaultValue false
*/
codeblock: z.union([z.object({
lang: z.string().optional(),
meta: z.string().optional()
}), z.boolean()]).default(false)
});
function fileGenerator({ relative = false, trim = true } = {}) {
return {
name: "file",
async run(input, ctx) {
const { file, codeblock = false } = fileGeneratorSchema.parse(input);
const dest = relative ? path.resolve(ctx.cwd, path.dirname(ctx.path), file) : path.resolve(ctx.cwd, file);
let value = await fs.readFile(dest, "utf-8");
if (trim) value = value.trim();
if (codeblock === false) return {
type: "paragraph",
children: [{
type: "text",
value
}]
};
const codeOptions = codeblock === true ? {} : codeblock;
return {
type: "code",
lang: codeOptions.lang ?? path.extname(dest).slice(1),
meta: codeOptions.meta,
value
};
}
};
}
//#endregion
//#region src/remark-install.ts
/**
* It generates the following structure from a code block with `package-install` as language
*
* @example
* ```tsx
* <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
* <Tab value="pnpm">...</Tab>
* ...
* </Tabs>
* ```
*
* @deprecated Use `remarkNpm` from `fumadocs-core/mdx-plugins` instead, it's a drop-in replacement.
*/
function remarkInstall({ Tab = "Tab", Tabs = "Tabs", persist = false, packageManagers = [
{
command: (cmd) => convert(cmd, "npm"),
name: "npm"
},
{
command: (cmd) => convert(cmd, "pnpm"),
name: "pnpm"
},
{
command: (cmd) => convert(cmd, "yarn"),
name: "yarn"
},
{
command: (cmd) => convert(cmd, "bun"),
name: "bun"
}
] } = {}) {
return (tree) => {
visit(tree, "code", (node) => {
if (node.lang !== "package-install") return "skip";
const value = node.value.startsWith("npm") || node.value.startsWith("npx") ? node.value : `npm install ${node.value}`;
const insert = createElement(Tabs, [...typeof persist === "object" ? [{
type: "mdxJsxAttribute",
name: "groupId",
value: persist.id
}, {
type: "mdxJsxAttribute",
name: "persist",
value: null
}] : [], expressionToAttribute("items", {
type: "ArrayExpression",
elements: packageManagers.map(({ name }) => ({
type: "Literal",
value: name
}))
})], packageManagers.map(({ command, name }) => ({
type: "mdxJsxFlowElement",
name: Tab,
attributes: [{
type: "mdxJsxAttribute",
name: "value",
value: name
}],
children: [{
type: "code",
lang: "bash",
meta: node.meta,
value: command(value)
}]
})));
Object.assign(node, insert);
});
};
}
//#endregion
//#region src/remark-show.ts
function remarkShow(options) {
const variables = options?.variables ?? {};
return async (tree, file) => {
const { toJs } = await import("estree-util-to-js");
const tasks = [];
visit(tree, "mdxJsxFlowElement", (node) => {
if (node.name !== "show") return;
for (const attr of node.attributes) {
if (attr.type !== "mdxJsxAttribute" || attr.name !== "on") continue;
if (!attr.value || typeof attr.value !== "object" || !attr.value.data?.estree) return "skip";
const js = toJs(attr.value.data.estree);
const callback = new Function(...Object.keys(variables), `return ${js.value}`)(...Object.values(variables));
tasks.push((async () => {
const value = typeof callback === "function" ? await callback(file) : callback;
Object.assign(node, {
type: "mdxJsxFlowElement",
name: null,
attributes: [],
children: value === true ? node.children : []
});
})());
return "skip";
}
});
await Promise.all(tasks);
};
}
//#endregion
export { createElement, expressionToAttribute, fileGenerator, fileGeneratorSchema, remarkDocGen, remarkInstall, remarkShow };
//# sourceMappingURL=index.js.map