import ts from "typescript"; export interface DocsType { name?: string; type?: string; description?: string; default?: string; required?: boolean; version?: number | string; } export interface Interfaces { name?: { description?: string; /** 是否不渲染 markdown */ ignore?: string; props?: DocsType[]; }; } /** * Ts Interface 类型定义转换为 json */ export default class TsToJson { checker: any; interfaces: any; fileNames: any; types: any; _createProgram(fileNames: string[], options: ts.CompilerOptions) { this.interfaces = {}; this.types = {}; // 使用filename中的根文件名构建一个程序; const program = ts.createProgram(fileNames, options); // 获取检查器,我们将使用它来查找更多关于 ast 的信息 this.checker = program.getTypeChecker(); this.fileNames = fileNames; // 访问程序中的每个sourceFile for (const sourceFile of program.getSourceFiles()) { if (!sourceFile.isDeclarationFile) { // 遍历树以搜索 interface 和 type ts.forEachChild(sourceFile, (node) => this._visitAst(node)); } } } /** * 访问查找导出的节点 * @param node * @returns */ _visitAst(node: ts.Node) { // 只考虑导出的节点 if (!this._isNodeExported(node)) { return; } switch (ts.SyntaxKind[node.kind]) { case "InterfaceDeclaration": this.generateInterfaceDeclaration(node); break; case "TypeAliasDeclaration": this.generateTypeAliasDeclaration(node); break; default: break; } } /** * 获取 Interface 类型 json 数据 * @param node */ generateInterfaceDeclaration(node: ts.Node) { if ( // @ts-ignore node.parent?.fileName.replace(/\\/g, "/") !== this.fileNames[0].replace(/\\/g, "/") ) return; const newNode = node as ts.InterfaceDeclaration; const symbol = this.checker.getSymbolAtLocation(newNode.name); const { escapedName } = symbol; const { name = escapedName as string, ...rest } = this._getDocs(symbol); this.interfaces[name] = { ...rest, props: [], }; if (newNode.heritageClauses) { // 是否有extends const firstHeritageClause = newNode.heritageClauses[0]; const firstHeritageClauseType = firstHeritageClause.types![0]; const extendsType = this.checker.getTypeAtLocation( firstHeritageClauseType.expression ); if (extendsType?.symbol?.members && extendsType.symbol.members.size > 0) { // 接口 extends 接口(Interface) extendsType.symbol.members.forEach((member: any) => { this.interfaces[name].props.push(this._serializeSymbol(member)); }); } else if (firstHeritageClauseType) { console.log("暂时不支持接口继承Type类型"); // 接口 extends 类型(Type) // const type = this.checker.typeToTypeNode( // this.checker.getTypeAtLocation(firstHeritageClauseType), // firstHeritageClauseType, // ts.NodeBuilderFlags.InTypeAlias // // ts.TypeFormatFlags.InTypeAlias // ); // console.log(firstHeritageClauseType.getFullText()); } } symbol.members.forEach((member: any) => { this.interfaces[name].props.push(this._serializeSymbol(member)); }); } /** * 获取 Type 类型 json 数据 * @param node */ generateTypeAliasDeclaration(node: ts.Node) { const type = this.checker.typeToString( this.checker.getTypeAtLocation(node), node, ts.TypeFormatFlags.InTypeAlias ); const name = (node as any).name.escapedText; this.types[name] = type; } /** * 将symbol序列化为json对象 * @param symbol * @returns */ _serializeSymbol(symbol: ts.Symbol): DocsType | undefined { if (!symbol) return; const docs = this._getDocs(symbol); const questionToken = (symbol as any).valueDeclaration?.questionToken; const type = this.checker.typeToString( this.checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration) ); let newType = type; if (type.indexOf("|") > 0) { newType = type .split("|") .map((item: any) => { const hasType = this.types[item.trim()]; if (hasType) { return ` ${hasType} `; } return item; }) .join("|"); } else if (this.types[type]) { newType = this.types[type]; } return { name: symbol.getName(), required: !questionToken, type: newType, ...docs, }; } /** * 获取jsDoc信息 * @param symbol * @returns */ _getDocs(symbol: ts.Symbol): DocsType { const docs: DocsType = {}; if ( symbol.getJsDocTags && Array.isArray(symbol.getJsDocTags(this.checker)) ) { symbol.getJsDocTags(this.checker).forEach((tag) => { (docs as any)[tag.name] = tag.text && tag.text[0].text; }); } return docs; } /** * 如果在文件外部可见,则为True,否则为false * @param node * @returns boolean */ _isNodeExported(node: ts.Node): boolean { return ( (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0 || (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) ); } parse( fileNames: string, options: ts.CompilerOptions = { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS, } ) { this._createProgram([fileNames], options); return this.interfaces; } }