UNPKG

fumadocs-typescript

Version:

Typescript Integration for Fumadocs

513 lines (494 loc) 16.3 kB
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 };