使用ts-morph自动重写了大量的TypeScript代码

有一款名为ts-morph的工具,可以用于分析和重写TypeScript代码。
最近在工作中需要对大量的TypeScript文件进行重写,于是尝试使用了这个工具。它非常方便,并且学习使用方法也很简单,因此我打算结合实例来介绍一下。

我做了什么样的改写?

因为要根据实例进行介绍,所以首先简要介绍一下本次要进行的改写的内容。
本次进行的是与GraphQL代码生成库的迁移工作(apollo-tooling → GraphQL Code Generator)相关的改写工作。

由于代码生成库的更改,导入文件发生了变化,导入的名称也有一些改变,需要对其进行相应的修改。

具体而言,这样的代码可以这样写:

import FetchDataQuery from '~/graphql/generated/someQueryByQueryName.gql'
import { FetchData, FetchDataVariable, FetchData_items } from '~/graphql/generated/types/someQueryByQueryName'

export const SomeComponent = () => {
  const { data, refetch } = useQuery<FetchData, FetchDataVariables>(
    FetchDataQuery,
  )
  const items: FetchData_items[] = data.items
  // ...
)

请将以下内容用中文进行改述,仅需要一个选项:

import { FetchData, FetchDataDocument } from '~/graphql/generated/some-query-by-file-name'

export const SomeComponent = () => {
  const { data, refetch } = useQuery(
    FetchDataDocument,
  )
  const items: FetchData_items[] = data.items
  // ...
)

type FetchData_items = FetchData['items'][number]

在Qiita(一个网站)上有大约100个这样的文件需要修改,所以用手工方式全部重新写入是非常繁琐的。

我在这里使用的是 ts-morph。

ts-morph 能做些什么?

目的
TypeScript AST的设置、导航和操作可能是一个挑战。这个库封装了TypeScript编译器API,使其变得简单。
https://ts-morph.com/

ts-morph是一个用于操作TypeScript文件的库,它封装了TypeScript编译器的功能,以便于简单操作。
对于使用VSCode或TypeScript LSP的人来说,它提供了大部分可通过ts-morph来使用的重构功能(重命名、添加导入等)。

使用ts-morph进行代码重写

只要阅读 ts-morph 的官方文档 (ts-morph – Documentation),并试着操作一下,就能大致理解它。下面我将介绍一些使用 ts-morph 的实际案例。

使用 ts-morph 进行文件更改的流程

使用 ts-morph 操作文件的基本流程如下:获取每个文件(sourceFile),执行操作,并保存。

import { Project } from 'ts-morph'

const project = new Project({ tsConfigFilePath: './tsconfig.json' })
project.getSourceFiles().forEach(sourceFile => {
  sourceFile.getClasses() // sourceFile 内の class 定義をすべて取得する

  sourceFile.getImportDeclarations((importDeclaration) => { // sourceFile 内の各 import を取得する
    const specifier = importDeclaration.getModuleSpecifierValue() // import ファイル指定先
    const newSpecifier = specifier.replace(specifier, `modified/${specifier}`)
    importDeclaration.setModuleSpecifier(newSpecifier) // import ファイル指定先を変更する
  })

  const importDeclaration = sourceFile.addImportDeclaration({
    defaultImport: "MyClass",
    moduleSpecifier: "./file",
  }) // import MyClass from "./file" を追加する

  sourceFile.save() // 行った操作をファイルに反映させる
})

修改导入的名称

如果不仅仅是对其进行简单的改写,而且还要更改变量名,您可以一并在使用它的地方进行更改(类似于 LSP 提供的 rename 功能)。

import { FetchDataQuery } from '~/graphql/generated/types/some-query-by-file-name'

const { data, refetch } = useQuery(FetchDataQuery)

点击这里

import { FetchDataDocument } from '~/graphql/generated/types/some-query-by-file-name'

const { data, refetch } = useQuery(FetchDataDocument)

你可以使用以下代码一次性修改导入的名称以及使用该名称的地方。

const namedImports = importDeclaration.getNamedImports()
namedImports.forEach(namedImport => {
  const currentName = namedImport.getText()

  if (/Query$/.test(currentName)) {
    const newName = currentName.replace(/Query$/, 'Document')
    const aliasName = namedImport.getAliasNode()?.getText() // import { FetchDataQuery as Query } from ... という形式の場合は Query が alias

    if (aliasName) {
      // Alias がある場合は一度 Alias を削除してから、import する名前を変更して Alias を再設定する
      namedImport.removeAlias()
      namedImport.setName(newName)
      namedImport.setAlias(aliasName)
    } else {
      // Alias がない場合はそのまま名前を変更する。この書き方だと利用箇所も一緒に変更される
      namedImport.getNameNode().rename(newName)
    }
  }
})

添加定义

由于生成的代码中有一些名称不再导出,所以为了与其相对应,需要添加类型定义。

import { FetchData_items } from '~/graphql/generated/types/some-query-by-file-name'

请将以下内容用中文进行本地化改写,只需提供一个选项:

请使用中文将以下内容重新表达,只需提供一个选项:

import { FetchData } from '~/graphql/generated/some-query-by-file-name'

type FetchData_items = FetchData['items'][number]

由于ts-morph提供了一个用于添加Type定义的函数,因此可以利用它来添加上述的类型定义。

const namedImports = importDeclaration.getNamedImports()
namedImports.forEach(namedImport => {
  const currentName = namedImport.getNameNode().getText()
  if (/_/.test(currentName)) {
    const [basePart, ...props] = currentName
      .split('_') as string[]

    // FetchData_items -> FetchData['items'][number] という形式に変換する
    const aliasType = props.reduce((accType, prop) => {
      switch (prop) {
        case 'items':
          return `${accType}[${prop}][number]`
        default:
          return `${accType}[${prop}]`
      }
    }, basePart)

    // type 定義を追加する
    sourceFile.addTypeAlias({
      name: currentName,
      type: aliasType,
    })

    // import する名前を FetchData -> FetchData_items に変更する
    namedImport.setName(basePart)
  }
})

整理 import 定义,在 organize imports 中执行

当以机械方式进行代码重写时,可能会导致未使用的导入模块保留下来,添加的导入模块顺序混乱等,使得代码看起来不美观。

import FetchDataQuery from '~/graphql/generated/someQueryByQueryName.gql'
import { FetchData_items } from '~/graphql/generated/types/someQueryByQueryName'
import XXX from 'other-library'
import { FetchData } from '~/graphql/generated/some-query-by-file-name'

请将以下内容用中文进行本土化改写,只需要一种选择:

import { FetchData } from '~/graphql/generated/some-query-by-file-name'
import XXX from 'other-library'

在 ts-morph 中,有一个称为“整理导入”的功能,可以删除未使用的导入项,调整导入的顺序。

project.getSourceFiles().forEach(sourceFile => {
  sourceFile.organizeImports() // organize imports を実行する
  sourceFile.save() // 行った操作をファイルに反映させる
})

写代码时的实际修改技巧

经此一事,我们发现了 ts-morph 可以进行必要的重写操作。

在团队开发中,使用这个工具进行实际的重写是很常见的,与其他成员发生讨论也是常有的情况。下面将介绍在团队开发中进行重写工作时有用的方法。

提示1:将一系列的改写全部编码化,以便能够幂等地执行。

將以下內容以中文進行本地化改寫,只需提供一種選項:
對於改寫作業,基本上建議先將其編碼化,同時進行回滾操作,以確保腳本的冪等性。

# 書き換え実施前にコードの状態を戻す
git restore --source=$BASE_COMMIT_ID --worktree -- javascripts/

# 一連の書き換え作業を行う
ts-node ./rewriter-with-ts-morph.ts
yarn run format || true

很好的原因是,因为这样做有以下优点。

    • 書き換え作業では、バグや考慮漏れの対処など、試行錯誤が必要になるので、リトライしやすくしておくと効率が良い

 

    • 他のメンバーの変更への追従が容易になる

 

    • 作業を全てスクリプト化することで、意図しない作業の混入を防げる

 

    • レビューのタイミングでのフィードバックなど、後から出てきた要望にも対応しやすい

作業を始める前に、ある程度方向性などを握っておくのももちろん重要ですが、実物を見てコメントが来ることもあるので、それに対応しやすいというメリットは大きいです。

特别是,我认为能够容易地应对后来产生的细微反馈是非常重要的。
当然,在开始工作之前阶段的时候,与审核者进行某种程度的沟通也是很重要的,但是能够在查看更改后的代码时进行细致的讨论,能够高效地推进讨论,这是一件非常好的事情。

提示2:建议同时使用代码检查工具(如Linter)的自动修正功能和格式化工具。

这种代码重写工具经常会出现的情况是,重写或添加的代码往往不符合项目的缩进等写法。

由于重写工具很难根据项目的编写方式添加代码,所以基本上最好同时使用 Linter 的自动修复和格式化工具。

广告
将在 10 秒后关闭
bannerAds