214 lines
5.5 KiB
TypeScript
214 lines
5.5 KiB
TypeScript
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;
|
||
}
|
||
}
|