fumadocs-typescript
Version:
Typescript Integration for Fumadocs
400 lines (384 loc) • 12.8 kB
JavaScript
import {
__async,
__objRest,
__spreadProps,
__spreadValues,
renderMarkdownToHast
} from "./chunk-C45FGSN3.js";
// src/lib/base.ts
import {
Project as Project2,
ts
} from "ts-morph";
// src/create-project.ts
import { Project } from "ts-morph";
function createProject(options = {}) {
var _a;
return new Project({
tsConfigFilePath: (_a = options.tsconfigPath) != null ? _a : "./tsconfig.json",
skipAddingFilesFromTsConfig: true
});
}
// src/lib/base.ts
import fs3 from "fs";
// src/lib/type-table.ts
import * as fs from "fs/promises";
import { join } from "path";
function getTypeTableOutput(gen, _a, options) {
return __async(this, null, function* () {
var _b = _a, { name, type } = _b, props = __objRest(_b, ["name", "type"]);
const file = props.path && (options == null ? void 0 : options.basePath) ? join(options.basePath, props.path) : props.path;
let typeName = name;
let content = "";
if (file) {
content = (yield fs.readFile(file)).toString();
}
if (type && type.split("\n").length > 1) {
content += `
${type}`;
} else if (type) {
typeName != null ? typeName : typeName = "$Fumadocs";
content += `
export type ${typeName} = ${type}`;
}
const output = gen.generateDocumentation(
{ path: file != null ? file : "temp.ts", content },
typeName,
options
);
if (name && output.length === 0)
throw new Error(`${name} in ${file != null ? file : "empty file"} doesn't exist`);
return output;
});
}
// src/lib/cache.ts
import fs2 from "fs";
import { createHash } from "crypto";
import path from "path";
function createCache() {
const dir = path.join(process.cwd(), ".next/fumadocs-typescript");
try {
fs2.mkdirSync(dir, { recursive: true });
} catch (e) {
}
return {
write(input, data) {
const hash = createHash("SHA256").update(input).digest("hex").slice(0, 12);
fs2.writeFileSync(path.join(dir, `${hash}.json`), JSON.stringify(data));
},
read(input) {
const hash = createHash("SHA256").update(input).digest("hex").slice(0, 12);
try {
return JSON.parse(
fs2.readFileSync(path.join(dir, `${hash}.json`)).toString()
);
} catch (e) {
return;
}
}
};
}
// src/lib/base.ts
import path2 from "path";
function createGenerator(config) {
var _a;
const options = config instanceof Project2 ? {
project: config
} : config;
const cacheType = (_a = options == null ? void 0 : options.cache) != null ? _a : "fs";
const cache = cacheType === "fs" ? createCache() : null;
let instance;
function getProject() {
var _a2;
instance != null ? instance : instance = (_a2 = options == null ? void 0 : options.project) != null ? _a2 : createProject(options);
return instance;
}
return {
generateDocumentation(file, name, options2) {
var _a2;
const content = (_a2 = file.content) != null ? _a2 : fs3.readFileSync(path2.resolve(file.path)).toString();
const cacheKey = `${file.path}:${name}:${content}`;
if (cache) {
const cached = cache.read(cacheKey);
if (cached) return cached;
}
const sourceFile = getProject().createSourceFile(file.path, content, {
overwrite: true
});
const out = [];
for (const [k, d] of sourceFile.getExportedDeclarations()) {
if (name && name !== k) continue;
if (d.length > 1)
console.warn(
`export ${k} should not have more than one type declaration.`
);
out.push(generate(getProject(), k, d[0], options2));
}
cache == null ? void 0 : cache.write(cacheKey, out);
return out;
},
generateTypeTable(props, options2) {
return getTypeTableOutput(this, props, options2);
}
};
}
function generateDocumentation(file, name, content, options = {}) {
var _a;
const gen = createGenerator((_a = options.project) != null ? _a : options.config);
return gen.generateDocumentation({ path: file, content }, name, options);
}
function generate(program, name, declaration, { allowInternal = false, transform } = {}) {
var _a;
const entryContext = {
transform,
program,
type: declaration.getType(),
declaration
};
const comment = (_a = declaration.getSymbol()) == null ? void 0 : _a.compilerSymbol.getDocumentationComment(
program.getTypeChecker().compilerObject
);
return {
name,
description: comment ? ts.displayPartsToString(comment) : "",
entries: declaration.getType().getProperties().map((prop) => getDocEntry(prop, entryContext)).filter(
(entry) => entry && (allowInternal || !("internal" in entry.tags))
)
};
}
function getDocEntry(prop, context) {
var _a, _b, _c, _d;
const { transform, program } = context;
if (context.type.isClass() && prop.getName().startsWith("#")) {
return;
}
const subType = program.getTypeChecker().getTypeOfSymbolAtLocation(prop, context.declaration);
const tags = Object.fromEntries(
prop.getJsDocTags().map((tag) => [tag.getName(), ts.displayPartsToString(tag.getText())])
);
let typeName = subType.getText(
void 0,
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope
);
if (subType.getAliasSymbol() && subType.getAliasTypeArguments().length === 0) {
typeName = (_b = (_a = subType.getAliasSymbol()) == null ? void 0 : _a.getEscapedName()) != null ? _b : typeName;
}
if (prop.isOptional() && typeName.endsWith("| undefined")) {
typeName = typeName.slice(0, typeName.length - "| undefined".length).trimEnd();
}
if ("remarks" in tags) {
typeName = (_d = (_c = new RegExp("^`(?<name>.+)`").exec(tags.remarks)) == null ? void 0 : _c[1]) != null ? _d : typeName;
}
const entry = {
name: prop.getName(),
description: ts.displayPartsToString(
prop.compilerSymbol.getDocumentationComment(
program.getTypeChecker().compilerObject
)
),
tags,
type: typeName,
required: !prop.isOptional(),
deprecated: prop.getJsDocTags().some((tag) => tag.getName() === "deprecated")
};
transform == null ? void 0 : transform.call(context, entry, subType, prop);
return entry;
}
// src/lib/mdx.ts
import * as path3 from "path";
var regex = new RegExp("^---type-table---\\r?\\n(?<file>.+?)(?:#(?<name>.+))?\\r?\\n---end---$", "gm");
var defaultTemplates = {
block: (doc, c) => `### ${doc.name}
${doc.description}
<div className='*:border-b [&>*:last-child]:border-b-0'>${c}</div>`,
property: (c) => `<div className='text-sm text-fd-muted-foreground py-4'>
<div className="flex flex-row items-center gap-4">
<code className="text-sm">${c.name}</code>
<code className="text-fd-muted-foreground">{${JSON.stringify(c.type)}}</code>
</div>
${c.description || "No Description"}
${Object.entries(c.tags).map(([tag, value]) => `- ${tag}:
${replaceJsDocLinks(value)}`).join("\n")}
</div>`
};
function generateMDX(generator, source, _a = {}) {
var _b = _a, { basePath = "./", templates: overrides } = _b, rest = __objRest(_b, ["basePath", "templates"]);
const templates = __spreadValues(__spreadValues({}, defaultTemplates), overrides);
return source.replace(regex, (...args) => {
const groups = args[args.length - 1];
const file = path3.resolve(basePath, groups.file);
const docs = generator.generateDocumentation(
{ path: file },
groups.name,
rest
);
return docs.map(
(doc) => templates.block(doc, doc.entries.map(templates.property).join("\n"))
).join("\n\n");
});
}
function replaceJsDocLinks(md) {
return md.replace(new RegExp("{@link (?<link>[^}]*)}", "g"), "$1");
}
// src/lib/file.ts
import * as path4 from "path";
import { mkdir, writeFile, readFile as readFile2 } from "fs/promises";
import { glob } from "tinyglobby";
function generateFiles(generator, options) {
return __async(this, null, function* () {
const files = yield glob(options.input, options.globOptions);
const produce = files.map((file) => __async(null, null, function* () {
const absolutePath = path4.resolve(file);
const outputPath = typeof options.output === "function" ? options.output(file) : path4.resolve(
options.output,
`${path4.basename(file, path4.extname(file))}.mdx`
);
const content = (yield readFile2(absolutePath)).toString();
let result = generateMDX(generator, content, __spreadValues({
basePath: path4.dirname(absolutePath)
}, options.options));
if (options.transformOutput) {
result = options.transformOutput(outputPath, result);
}
yield write(outputPath, result);
console.log(`Generated: ${outputPath}`);
}));
yield Promise.all(produce);
});
}
function write(file, content) {
return __async(this, null, function* () {
yield mkdir(path4.dirname(file), { recursive: true });
yield writeFile(file, content);
});
}
// src/lib/remark-auto-type-table.tsx
import { valueToEstree } from "estree-util-value-to-estree";
import { visit } from "unist-util-visit";
import { toEstree } from "hast-util-to-estree";
import { dirname as dirname2 } from "path";
function mapProperty(entry, renderMarkdown) {
return __async(this, null, function* () {
const value = valueToEstree({
type: entry.type,
default: entry.tags.default || entry.tags.defaultValue,
required: entry.required
});
if (entry.description) {
const hast = toEstree(yield renderMarkdown(entry.description), {
elementAttributeNameCase: "react"
}).body[0];
value.properties.push({
type: "Property",
method: false,
shorthand: false,
computed: false,
key: {
type: "Identifier",
name: "description"
},
kind: "init",
value: hast.expression
});
}
return {
type: "Property",
method: false,
shorthand: false,
computed: false,
key: {
type: "Literal",
value: entry.name
},
kind: "init",
value
};
});
}
function remarkAutoTypeTable({
name = "auto-type-table",
outputName = "TypeTable",
renderMarkdown = renderMarkdownToHast,
options = {},
remarkStringify = true,
generator = createGenerator()
} = {}) {
return (tree, file) => __async(null, null, function* () {
const queue = [];
let basePath = options == null ? void 0 : options.basePath;
if (!basePath && file.path) basePath = dirname2(file.path);
visit(tree, "mdxJsxFlowElement", (node) => {
if (node.name !== name) return;
const props = {};
for (const attr of node.attributes) {
if (attr.type !== "mdxJsxAttribute" || typeof attr.value !== "string")
throw new Error(
"`auto-type-table` does not support non-string attributes"
);
props[attr.name] = attr.value;
}
function run() {
return __async(this, null, function* () {
const output = yield generator.generateTypeTable(
props,
__spreadProps(__spreadValues({}, options), {
basePath
})
);
const rendered = output.map((doc) => __async(null, null, function* () {
const properties = yield Promise.all(
doc.entries.map((entry) => mapProperty(entry, renderMarkdown))
);
return {
type: "mdxJsxFlowElement",
name: outputName,
attributes: [
{
type: "mdxJsxAttribute",
name: "type",
value: {
type: "mdxJsxAttributeValueExpression",
value: remarkStringify ? JSON.stringify(doc, null, 2) : "",
data: {
estree: {
type: "Program",
sourceType: "module",
body: [
{
type: "ExpressionStatement",
expression: {
type: "ObjectExpression",
properties
}
}
]
}
}
}
}
],
children: []
};
}));
Object.assign(node, {
type: "root",
attributes: [],
children: yield Promise.all(rendered)
});
});
}
queue.push(run());
return "skip";
});
yield Promise.all(queue);
});
}
export {
createGenerator,
createProject,
generateDocumentation,
generateFiles,
generateMDX,
remarkAutoTypeTable,
renderMarkdownToHast
};