fumadocs-typescript
Version:
Typescript Integration for Fumadocs
513 lines (494 loc) • 16.3 kB
JavaScript
import {
__async,
__objRest,
__spreadProps,
__spreadValues,
parseTags,
renderMarkdownToHast,
renderTypeToHast
} from "./chunk-HM6PM4II.js";
// src/lib/base.ts
import {
Project as Project2,
ts as ts2
} 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";
// src/lib/get-simple-form.ts
import * as ts from "ts-morph";
function getSimpleForm(type, checker, noUndefined = false) {
if (type.isUndefined() && noUndefined) return "";
const alias = type.getAliasSymbol();
if (alias) {
return alias.getDeclaredType().getText(
void 0,
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope
);
}
if (type.isUnion()) {
const types = [];
for (const t of type.getUnionTypes()) {
const str = getSimpleForm(t, checker, noUndefined);
if (str.length > 0 && str !== "never") types.unshift(str);
}
return types.length > 0 ? (
// boolean | null will become true | false | null, need to ensure it's still returned as boolean
types.join(" | ").replace("true | false", "boolean")
) : "never";
}
if (type.isIntersection()) {
const types = [];
for (const t of type.getIntersectionTypes()) {
const str = getSimpleForm(t, checker, noUndefined);
if (str.length > 0 && str !== "never") types.unshift(str);
}
return types.join(" & ");
}
if (type.isTuple()) {
const elements = type.getTupleElements().map((t) => getSimpleForm(t, checker)).join(", ");
return `[${elements}]`;
}
if (type.isArray() || type.isReadonlyArray()) {
return "array";
}
if (type.getCallSignatures().length > 0) {
return "function";
}
if (type.isClassOrInterface() || type.isObject()) {
return "object";
}
return type.getText(
void 0,
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope
);
}
// src/lib/base.ts
function createGenerator(config) {
var _a;
const options = config instanceof Project2 ? {
project: config
} : config;
const cacheType = (_a = options == null ? void 0 : options.cache) != null ? _a : process.env.NODE_ENV !== "development" ? "fs" : false;
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 ? ts2.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;
const { transform, program } = context;
if (context.type.isClass() && prop.getName().startsWith("#")) {
return;
}
const subType = program.getTypeChecker().getTypeOfSymbolAtLocation(prop, context.declaration);
const isOptional = prop.isOptional();
const tags = prop.getJsDocTags().map(
(tag) => ({
name: tag.getName(),
text: ts2.displayPartsToString(tag.getText())
})
);
let simplifiedType = getSimpleForm(
subType,
program.getTypeChecker(),
isOptional
);
const remarksTag = tags.find((tag) => tag.name === "remarks");
if (remarksTag) {
const match = (_a = new RegExp("^`(?<name>.+)`").exec(remarksTag.text)) == null ? void 0 : _a[1];
if (match) simplifiedType = match;
}
const entry = {
name: prop.getName(),
description: ts2.displayPartsToString(
prop.compilerSymbol.getDocumentationComment(
program.getTypeChecker().compilerObject
)
),
tags,
type: getFullType(subType),
simplifiedType,
required: !isOptional,
deprecated: tags.some((tag) => tag.name === "deprecated")
};
transform == null ? void 0 : transform.call(context, entry, subType, prop);
return entry;
}
function getFullType(type) {
const alias = type.getAliasSymbol();
if (alias) {
return alias.getDeclaredType().getText(void 0, ts2.TypeFormatFlags.NoTruncation);
}
return type.getText(
void 0,
ts2.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope | ts2.TypeFormatFlags.NoTruncation | // we use `InTypeAlias` to force TypeScript to extend generic types like `ExtractParams<T>` into more detailed forms like `T extends string? ExtractParamsFromString<T> : never`.
ts2.TypeFormatFlags.InTypeAlias
);
}
// 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.simplifiedType)}}</code>
</div>
Full Type: <code className="text-fd-muted-foreground">{${JSON.stringify(c.type)}}</code>
${c.description || "No Description"}
${c.tags.map((tag) => `- ${tag.name}:
${replaceJsDocLinks(tag.text)}`).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.ts
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 objectBuilder() {
const out = {
type: "ObjectExpression",
properties: []
};
return {
addExpressionNode(key, expression) {
out.properties.push({
type: "Property",
method: false,
shorthand: false,
computed: false,
key: {
type: "Identifier",
name: key
},
kind: "init",
value: expression
});
},
addJsxProperty(key, hast) {
const estree = toEstree(hast, {
elementAttributeNameCase: "react"
}).body[0];
this.addExpressionNode(key, estree.expression);
},
build() {
return out;
}
};
}
function buildTypeProp(_0, _1) {
return __async(this, arguments, function* (entries, {
renderMarkdown = renderMarkdownToHast,
renderType = renderTypeToHast
}) {
function onItem(entry) {
return __async(this, null, function* () {
const node = objectBuilder();
node.addJsxProperty("type", yield renderType(entry.simplifiedType));
node.addJsxProperty("typeDescription", yield renderType(entry.type));
node.addExpressionNode("required", valueToEstree(entry.required));
const tags = parseTags(entry.tags);
if (tags.default)
node.addJsxProperty("default", yield renderType(tags.default));
if (tags.returns)
node.addJsxProperty("returns", yield renderMarkdown(tags.returns));
if (tags.params) {
node.addExpressionNode("parameters", {
type: "ArrayExpression",
elements: yield Promise.all(tags.params.map(onParam))
});
}
if (entry.description) {
node.addJsxProperty(
"description",
yield renderMarkdown(entry.description)
);
}
return node.build();
});
}
function onParam(param) {
return __async(this, null, function* () {
const node = objectBuilder();
node.addExpressionNode("name", valueToEstree(param.name));
if (param.description)
node.addJsxProperty(
"description",
yield renderMarkdown(param.description)
);
return node.build();
});
}
const prop = objectBuilder();
const output = yield Promise.all(
entries.map((entry) => __async(null, null, function* () {
return {
name: entry.name,
node: yield onItem(entry)
};
}))
);
for (const node of output) {
prop.addExpressionNode(node.name, node.node);
}
return prop.build();
});
}
function remarkAutoTypeTable(config = {}) {
const {
name = "auto-type-table",
outputName = "TypeTable",
options: generateOptions = {},
remarkStringify = true,
generator = createGenerator()
} = config;
return (tree, file) => __async(null, null, function* () {
const queue = [];
const defaultBasePath = file.path ? dirname2(file.path) : void 0;
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* () {
var _a;
const output = yield generator.generateTypeTable(
props,
__spreadProps(__spreadValues({}, generateOptions), {
basePath: (_a = generateOptions.basePath) != null ? _a : defaultBasePath
})
);
const rendered = output.map((doc) => __async(null, null, function* () {
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: yield buildTypeProp(doc.entries, config)
}
]
}
}
}
}
],
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
};