使用 AST 解析 React TypeScript Component 接口定义
TypeScript React Markdown props
props
import React from "react";
export interface IProps {
//
text: string;
//
onClick: () => void;
}
const Button = ({ text }) => {
return <button>{text}</button>;
};
export default Button;
IProps
AST
AST
AST
AST
interface
interface
definition
code -> ast -> interface -> definitions
AST
babel
const parser = require("@babel/parser");
function transformCode2Ast(code) {
return parser.parse(code, {
sourceType: "module",
plugins: [
"jsx",
"typescript",
"asyncGenerators",
"bigInt",
"classProperties",
"classPrivateProperties",
"classPrivateMethods",
["decorators", { decoratorsBeforeExport: false }],
"doExpressions",
"dynamicImport",
"exportDefaultFrom",
"exportNamespaceFrom",
"functionBind",
"functionSent",
"importMeta",
"logicalAssignment",
"nullishCoalescingOperator",
"numericSeparator",
"objectRestSpread",
"optionalCatchBinding",
"optionalChaining",
["pipelineOperator", { proposal: "minimal" }],
"throwExpressions",
"topLevelAwait",
"estree",
],
});
}
Button AST
Node {
type: 'File',
start: 0,
end: 207,
loc:
SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 16, column: 0 } },
errors: [],
program:
Node {
type: 'Program',
start: 0,
end: 207,
loc: SourceLocation { start: [Position], end: [Position] },
sourceType: 'module',
interpreter: null,
body: [ [Node], [Node], [Node], [Node] ] },
comments:
[ { type: 'CommentLine',
value: ' ',
start: 57,
end: 62,
loc: [SourceLocation] },
{ type: 'CommentLine',
value: ' ',
start: 81,
end: 88,
loc: [SourceLocation] } ] }
AST
ast-types
ast
const { visit } = require("ast-types");
function findInterface(ast) {
let ret = Object.create(null);
let currentInterface = null;
visit(ast, {
visitTSInterfaceDeclaration(nodePath) {
currentInterface = nodePath.value.id.name;
this.traverse(nodePath);
},
visitTSPropertySignature(nodePath) {
ret[currentInterface] = ret[currentInterface] || [];
ret[currentInterface].push(nodePath.value);
return false;
},
});
return ret;
}
AST
Interface
Node {
type: 'TSPropertySignature',
start: 65,
end: 78,
loc: [SourceLocation],
key: [Node],
computed: false,
typeAnnotation: [Node],
leadingComments: [Array],
trailingComments: [Array] },
Node {
type: 'TSPropertySignature',
start: 91,
end: 111,
loc: [SourceLocation],
key: [Node],
computed: false,
typeAnnotation: [Node],
leadingComments: [Array] } ] }
typeAnnotation
typeAnnotation
const get = require("lodash/get");
function parseTSTypeReference(typeName) {
const type = get(typeName, "type");
switch (type) {
case "TSQualifiedName":
return `${get(typeName, "left.name")}.${get(typeName, "right.name")}`;
default:
return `Unknown ReferenceType`;
}
}
function parseTSFunctionType(parameters, typeAnnotation) {
const parseTSFunctionParameters = (parameters) => {
if (!parameters || !parameters.length) {
return `()`;
}
let args = parameters.map((parameter) => {
return `${get(parameter, "name")}: ${parseTypeAnnotation(
get(parameter, "typeAnnotation.typeAnnotation")
)}`;
});
return "( " + args.join(", ") + ")";
};
const parseTSFunctionReturn = (typeAnnotation) => {
const type = get(typeAnnotation, "type");
switch (type) {
case "TSVoidKeyword":
return "void";
case "TSTypeReference":
return parseTSTypeReference(get(typeAnnotation, "typeName"));
default:
return `Unknown FunctionType`;
}
};
return `${parseTSFunctionParameters(parameters)} => ${parseTSFunctionReturn(
typeAnnotation
)}`;
}
function parseTSTypeLiteral(members) {
const ret = parseInterfaceDefinitions(members);
let args = ret.map((t) => `${t.name}: ${t.type}`);
return "{ " + args.join(", ") + " }";
}
function parseTypeAnnotation(typeAnnotation) {
const type = get(typeAnnotation, "type");
switch (type) {
case "TSNumberKeyword":
case "TSStringKeyword":
case "TSBoleanKeyword":
case "TSNullKeyword":
case "TSUndefinedKeyword":
case "TSSymbolKeyword":
case "TSAnyKeyword":
return type.match(/TS(\w+)Keyword/)[1].toLowerCase();
case "TSUnionType":
return get(typeAnnotation, "types", [])
.map((type) => get(type, "literal.value"))
.join(" | ");
case "TSFunctionType":
return parseTSFunctionType(
get(typeAnnotation, "parameters"),
get(typeAnnotation, "typeAnnotation.typeAnnotation")
);
case "TSTypeReference":
return parseTSTypeReference(get(typeAnnotation, "typeName"));
case "TSTypeLiteral":
return parseTSTypeLiteral(get(typeAnnotation, "members"));
default:
return "UnKnowType";
}
}
function parseInterfaceDefinitions(nodePaths) {
const parseInterfaceDefinitionsNode = (nodePath) => {
const name = get(nodePath, "key.name");
const comments = get(nodePath, "leadingComments.0.value", "")
.trim()
.split(/[\r\n]/)
.map((str) => str.trim().replace(/^\*/g, "").trim())
.filter(Boolean);
const typeAnnotation = get(nodePath, "typeAnnotation.typeAnnotation");
const type = parseTypeAnnotation(typeAnnotation);
return { name, type, comments };
};
return nodePaths.map(parseInterfaceDefinitionsNode);
}
Button
[
[
{
name: "text",
type: "string",
comments: [""],
},
{
name: "onClick",
type: "() => void",
comments: [""],
},
],
];
function parseTypeScriptComponentInterface(code) {
let ast = transformCode2Ast(code);
let interfaces = findInterfaces(ast);
let definitions = Object.keys(interfaces).reduce((a, c) => {
a[c] = a[c] || [];
a[c].push(parseInterfaceDefinitions(interfaces[c]));
return a;
}, Object.create(null));
return definitions;
}
const code = `
import React from 'react';
export interface IProps {
/**
* button
*
*/
text: string;
//
onClick: () => void;
// 3
props3: (arg: any) => void;
// 4
props4: (arg: { name: string, age: number }) => React.Node
}
const Button = ({ text }) => {
return <button>{text}</button>;
};
export default Button;
`;
let ret = parseTypeScriptComponentInterface(code);
{
"IProps": [
[
{
"name": "text",
"type": "string",
"comments": [
"button",
""
]
},
{
"name": "onClick",
"type": "() => void",
"comments": [
""
]
},
{
"name": "props3",
"type": "( arg: any) => void",
"comments": [
" 3"
]
},
{
"name": "props4",
"type": "( arg: { name: string, age: number }) => React.Node",
"comments": [
" 4"
]
}
]
]
}