团队使用 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
站在巨人的肩膀上,使用 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-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"
]
}
]
]
}