写一个基于 typescript compiler 的转换工具
> 背景:当你接手一些老的前端项目的时候,有时候这个项目同时又js和ts,如何快速将一个老的基于React的项目快速转换为ts风格。
## [ts compiler](microsoft/TypeScript)
### 链接是typescript官方的介绍,这里我简单说一说一些ts compiler的基本概念。
- Parser 根据源代码生产ast
- Type Checker 可以分析生成一些推断类型
- Emitter 生成器
- Pre-processor 分析源代码依赖,生成一系列SourceFile
## 目标
### 基本上我们需要将一个jsx风格的React Component 转换为一个tsx风格的Component
import PropTypes from prop-types
import React, { Component } from react
import { connect } from react-redux
import { bindActionCreators } from redux
@connect(
(state, props) => {
const { app, global, info, list, curItem } = state
return {
curApp: list[ProPS - Uplifting Publisher],
app,
curItem,
total: info.total,
name: global.name,
}
},
dispatch =>
bindActionCreators(
{
save,
update,
remove,
},
dispatch
)
)
export default class Test extends Component {
static propTypes = {
obj: PropTypes.object,
isBool: PropTypes.bool,
str: PropTypes.string,
arr: PropTypes.array,
oneOfType: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
node: PropTypes.node,
oneOf: PropTypes.oneOf([a, b, c, d]),
func: PropTypes.func,
required: PropTypes.func.isRequired,
}
constructor(props) {
super(props)
this.state = {
isShowModal: false,
modalName: props.report_name || ,
modalType: save,
confirmLoading: false,
monitorModalVisible: false,
}
this.aaa = 111
}
render() {
return <div>hello tscer</div>
}
}
### 我们需要做什么
- 将 static propTypes 转换为 interface IProps
- 根据 this.state 生成 interface IState
- 生成 Component 的 generic types
- 去掉 PropTypes
### 在开始之前我们需要个 ast viewer 帮我们快速分析ast树结构, [这里](TypeScript AST Viewer),让我们把上述代码复制进去, 看看ast树结构。
## 开始分析
### 入口
import { CompilerOptions, createPrinter, createProgram, EmitHint, transform } from typescript
const program = createProgram([realPath], compileOptions)
const sourceFiles = program
.getSourceFiles()
.filter(s => s.fileName === realPath)
const typeChecker = program.getTypeChecker()
const result = transform(sourceFiles, [
generateGenericPropAndState(typeChecker),
removeImportPropTypes(typeChecker),
removeStaticPropTypes(typeChecker),
])
const printer = createPrinter()
const printed = printer.printNode(
EmitHint.SourceFile,
result.transformed[0],
sourceFiles[0]
)
const res = prettier.format(printed, {
semi: true,
singleQuote: true,
trailingComma: es5,
bracketSpacing: true,
parser: typescript,
})
- createProgram
- 第一个参数是我们需要编译文件的地址数组
- 第二个参数是ts项目下面的tsconfig.json的一些编译选项,这里可以读取目标文件的compileOptions,或者自选一些默认的选项生成。
- 简单来说一个program就是编译的入口(pre-processor),根据的参数ts compiler会生成一系列目标文件编译所依赖的库,生成一系列SourceFiles
- program.getSourceFiles()
- 获取编译后的SourceFile对象,这里需要filter一下我们需要的目标文件,因为默认ts compiler会添加一系列目标文件所依赖的一些文件,例如一个es dom lib库等, 我们需要的只是目标文件。
- transform
- 类似于babel分析里面的 traverse,本质就是便利ast,分析每个node, 然后做一个我们需要的操作
- generateGenericPropAndState 做的就是生成IProps 和 IState
- removeImportPropTypes 删除掉 import PropTypes
- removeStaticPropTypes 删除掉 static PropTypes
- createPrinter
- Printer 就是最后的generator, 生成最终我们的代码
- 最后可以利用 prettier, tslint等格式化工具生成你项目中需要的代码
### removeImportPropTypes
import { isImportDeclaration, isStringLiteral, SourceFile, updateSourceFileNode } from typescript
export const removeImportPropTypes = (typeChecker: TypeChecker) => (
context: TransformationContext
) => (sourceFile: SourceFile) => {
const statements = sourceFile.statements.filter(
s =>
!(
isImportDeclaration(s) &&
isStringLiteral(s.moduleSpecifier) &&
s.moduleSpecifier.text === prop-types
)
)
return updateSourceFileNode(sourceFile, statements)
}
- 这里更多的会将思考过程,代码细节大家可以自己去试试就知道了
- 一个transform的高阶方程
- 第一个参数 typeChecker,使我们在transform中自己传递的
- 第二个参数 context, 使整个编译过程中保存的上下文信息
- 第三个参数 sourceFile,就是我们需要编译的源文件了
- sourceFile的结构, 这里就用到我之前说的[ast viewwe](TypeScript AST Viewer)
- 中间的SourceFile即是sourceFile的结构了,选择代码也可以看到代码对应的ast结构
- 这里我们需要把 import PropTypes from prop-types 删除掉,明细这里对应的是一个叫做 ImportDeclaration 的结构
- 我们看看图中最右侧Node节点
- 我们需要的是一个叫做 prop-types的import 声明,很明显在右侧它在 moduleSpecifier -> text 里面
- 到这里我们就得到我们需要的了 找到一个sourceFile里面 ImportDeclaration的moduleSpecifier的text是prop-types的节点,去除掉即可。
- sourceFile.statements 代表的是每一个代码块
- filter就按照我以上说的逻辑 去除掉 prop-types
- 最后返回 updateSourceFileNode, 生成了一个更新后我们需要新的sourceFile返回
- 之后的transform功能类似于此的思考过程,由于ts结构vscode有很好的代码提示,以及类型注释,一些ts compiler的api大家根据对应Node的定义应该可以很快的适应
### removeStaticPropTypes
export const removeStaticPropTypes = (typeChecker: TypeChecker) => (
context: TransformationContext
) => (sourceFile: SourceFile) => {
const visitor = (node: Node) => {
if (isClassDeclaration(node) && isReactClassComponent(node, typeChecker)) {
return updateClassDeclaration(
node,
node.decorators,
node.modifiers,
node.name,
node.typeParameters,
createNodeArray(node.heritageClauses),
node.members.filter(m => {
if (
isPropertyDeclaration(m) &&
isStaticMember(m) &&
isPropTypesMember(m)
) {
// static and propTypes
return false
}
if (
isGetAccessorDeclaration(m) &&
isStaticMember(m) &&
isPropTypesMember(m)
) {
// static and propTypes
return false
}
return true
})
)
}
return node
}
return visitEachChild(sourceFile, visitor, context)
}
- visitEachChild 遍历ast tree,在每一个node给一个回调
- 我们需要去除掉 static propTypes, 前提如图, 首先是一个ClassDeclaration,其次是一个 React Component, 最后class里面有一个 PropertyDeclaration, static修饰,名字是propTypes
- ClassDeclaration很好判断, isClassDeclaration
- react Component, 这里我们需要分析 ClassDeclaration 的 HeritageClause,也就是继承条件。如图展示,我们需要获得 HeritageClause.types[0].expression.getText(), 这里可以利用正则去判断一下,/React.Component|Component|PureComponent|React.PureComponent/, 基本情况下这是react class component
- isPropertyDeclaration可以判断是否是 PropertyDeclaration,如图,PropertyDeclaration.modifiers[0] === StaticKeyword, 这里判断其修饰是否是一个 static, PropertyDeclaration.name === propTypes 去判断。
### generateGenericPropAndState
- 思考过程还是如上面所述,大家可以先自己尝试一下。 [源码](sonacy/tscer)
- 关于生成的新的代码的ast结构, 大家可以在 [ast viewer](TypeScript AST Viewer)输入需要的代码,在观察一下生成的ast结构,从而去构建例如 interface 等类型结构。
## 总结
### 还可以做什么
- 例如将 redux connect转换为hoc,利用 returnType<typeof mapStateToProps> 获取redux的一些类型
- 可以根据 class 里面 this.props 去分析, 添加一个没有定义的属性添加到 IProps 中间
- 给一些声明周期添加一些IProps, IState, params的参数
- 可以将一些 简单function, 利用checker生成一些类型,并在复杂类型添加TODO,给后期方便添加。
- 可以根据一些project的目录,批量去处理js, jsx去转换成需要的ts文件
- 做一个回退操作, 如果不理想, 用户可以回退为原来的js文件
- 。。。
### ts compile 可以根据大家的需要做一些大家预期的操作,类似于babel的parser, traverse, generator等。这里只是大概的思路提供给大家,毕竟现在很多小程序框架也是类似的方式。happy coding!
推荐阅读: