寫一個基於 typescript compiler 的轉換工具

背景:當你接手一些老的前端項目的時候,有時候這個項目同時又js和ts,如何快速將一個老的基於React的項目快速轉換爲ts風格。前端

ts compiler

連接是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.id],
      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樹結構, 這裏,讓咱們把上述代碼複製進去, 看看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',
  })
複製代碼
  • createProgramnode

    • 第一個參數是咱們須要編譯文件的地址數組
    • 第二個參數是ts項目下面的tsconfig.json的一些編譯選項,這裏能夠讀取目標文件的compileOptions,或者自選一些默認的選項生成。
    • 簡單來講一個program就是編譯的入口(pre-processor),根據的參數ts compiler會生成一系列目標文件編譯所依賴的庫,生成一系列SourceFiles
  • program.getSourceFiles()react

    • 獲取編譯後的SourceFile對象,這裏須要filter一下咱們須要的目標文件,由於默認ts compiler會添加一系列目標文件所依賴的一些文件,例如一個es dom lib庫等, 咱們須要的只是目標文件。
  • transformgit

    • 相似於babel分析裏面的 traverse,本質就是便利ast,分析每一個node, 而後作一個咱們須要的操做
    • generateGenericPropAndState 作的就是生成IProps 和 IState
    • removeImportPropTypes 刪除掉 import PropTypes
    • removeStaticPropTypes 刪除掉 static PropTypes
  • createPrintergithub

    • 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)
}

複製代碼
  • 這裏更多的會將思考過程,代碼細節你們能夠本身去試試就知道了typescript

  • 一個transform的高階方程express

    • 第一個參數 typeChecker,使咱們在transform中本身傳遞的
    • 第二個參數 context, 使整個編譯過程當中保存的上下文信息
    • 第三個參數 sourceFile,就是咱們須要編譯的源文件了
  • sourceFile的結構, 這裏就用到我以前說的ast viewwejson

  • 中間的SourceFile便是sourceFile的結構了,選擇代碼也能夠看到代碼對應的ast結構redux

  • 這裏咱們須要把 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

  • 思考過程仍是如上面所述,你們能夠先本身嘗試一下。 源碼
  • 關於生成的新的代碼的ast結構, 你們能夠在 ast viewer輸入須要的代碼,在觀察一下生成的ast結構,從而去構建例如 interface 等類型結構。

總結

還能夠作什麼

  • 例如將 redux connect轉換爲hoc,利用 returnType 獲取redux的一些類型
  • 能夠根據 class 裏面 this.props 去分析, 添加一個沒有定義的屬性添加到 IProps 中間
  • 給一些聲明週期添加一些IProps, IState, params的參數
  • 能夠將一些 簡單function, 利用checker生成一些類型,並在複雜類型添加TODO,給後期方便添加。
  • 能夠根據一些project的目錄,批量去處理js, jsx去轉換成須要的ts文件
  • 作一個回退操做, 若是不理想, 用戶能夠回退爲原來的js文件
  • 。。。

ts compile 能夠根據你們的須要作一些你們預期的操做,相似於babel的parser, traverse, generator等。這裏只是大概的思路提供給你們,畢竟如今不少小程序框架也是相似的方式。happy coding!

相關文章
相關標籤/搜索