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;
|
|||
|
}
|
|||
|
}
|