Help us understand the problem. What is going on with this article?

jscodeshift with TypeScript

はじめに

以前、jscodeshitという JavaScriptおよびTypeScriptコードのリファクタリングツールの紹介記事を書きました。

JavaScriptのリファクタリングツール「jscodeshift」の使い方

このツールは、ASTベースでコードを自在に変換でき、とても便利です。jscodeshiftは、Transform File(変換処理を記述するファイル)として変換処理を書くのですが、これまでJavaScriptで変換処理を書くことが多かったです。

しかし、Transform FileはTypeScriptで書くことが可能です。

最近、TypeScriptで変換処理を書いた際、その開発体験がとても良かったので紹介します。ASTベースのコーディングにTypeScriptの型推論や補完があると、開発が快適になることは想像しやすいでしょう。

この記事で出てくるコードやファイルは以下のリポジトリで確認できます。

jscodeshift-with-typescript

セットアップ

まずは、jscodeshiftのTransform FileをTypeScriptで書く準備をします。

ライブラリのインストール

必要なライブラリをインストールします。typescriptの他に、@types/jscodeshiftをインストールします。

$ npm i -D jscodeshift typescript @types/jscodeshift

tsconfig.json

tsconfig.jsonを作成します。設定はお好みで調整してください。

$ npx tsc --init

TypeScriptで変換処理を書く

セットアップが終わったら、TypeScriptでTransform Fileを作成します。

jscodeshift + TypeScriptの土台となる記述

jscodeshift + TypeScriptでのコーディングの土台となる記述を書きます。
型推論により、型注釈を消すことはできますが、説明の便宜上書いています。

import { Transform, FileInfo, API } from "jscodeshift";

const transform: Transform = ({ source }: FileInfo, { jscodeshift }: API) => {
  const j = jscodeshift;
  return j(source).toSource();
};

export default transform;

あとは、補完が効くので、快適に開発を進めていくことができます。

変換処理 foo.bar()をfoo.baz()に変換

前回の記事でもシンプルな例として出した、foo.bar()をfoo.baz()に変換処理を書いていきます。

といっても、主に利用する関数や値は型推論があるので、JavaScriptの時と比べても記述量は変わりません。規模が大きくなり、変換処理を関数として切り出す際などに型注釈を付与することになるでしょう。

対象のNodeを見つける

  // ...
  const j = jscodeshift;
  return j(source)
    .find(j.CallExpression, {
      callee: {
        object: { name: "foo" },
        property: { name: "bar" },
      },
    })
  // ...
};

export default transform;

find関数にわたすAST NodeのTypeを選択する際も、エディターでの補完の恩恵を得ることができ、非常に便利です。

Nodeを置き換える

    // ...
    .find(j.CallExpression, {
      callee: {
        object: { name: "foo" },
        property: { name: "bar" },
      },
    })
    .replaceWith((path) => {
      // TODO
    })
    // ....
};

別のAST Nodeへ置き換えるreplaceWith関数に渡ってくるpathオブジェクトの型も推論されます。

新しいNodeの作成

  const j = jscodeshift;
  // ...
    .replaceWith((path) => {
      return j.callExpression(
        j.memberExpression(j.identifier("foo"), j.identifier("baz")),
        path.value.arguments
      );
    })
  // ...
};

export default transform;


置き換えるAST Nodeを作成する際に、利用する便利なBuilderメソッド(e.g. j.memberExpression)へ渡すことが可能な値も検査してくれます。
JavaScriptのときは、このあたりが特に苦労したので、とても助かります。

成果物

最終的には以下のようなコードになりました。

transform.ts

import { Transform, FileInfo, API } from "jscodeshift";

const transform: Transform = ({ source }: FileInfo, { jscodeshift }: API) => {
  const j = jscodeshift;
  return j(source)
    .find(j.CallExpression, {
      callee: {
        object: { name: "foo" },
        property: { name: "bar" },
      },
    })
    .replaceWith((path) => {
      return j.callExpression(
        j.memberExpression(j.identifier("foo"), j.identifier("baz")),
        path.value.arguments
      );
    })
    .toSource();
};

export default transform;

動作確認

jscodeshiftコマンドにTransform Fileと対象ファイルを渡して動作確認しましょう。
-pはprintです。-dはdry runです。
また、対象ファイルとしてTypeScriptファイルを指定する場合は、--parserオプションでtsを指定します。(tsxも指定可能です)

$ npx jscodeshift -t transform.ts index.ts --parser ts -p -d
Processing 1 files...
Spawning 1 workers...
Running in dry mode, no files will be written!
Sending 1 files to free worker...
const foo: any = {};

foo.baz();

All done.
Results:
0 errors
0 unmodified
0 skipped
1 ok
Time elapsed: 0.855seconds

テスト

最後に、テストについても触れておきます。

jscodeshiftのテストについては以下の記事を見ると良いです。

jscodeshiftのテストを書く

今回はjscodeshiftのテストおよびテストのfixture fileにTypeScriptファイルを使う方法を紹介します。

ライブラリのインストール

jestts-jestをインストールします。

$ npm i -D jest ts-jest

jestのセットアップ

jest.config.jspresetts-jestを指定します。

module.exports = {
  preset: "ts-jest",
};

fixture fileとテストファイルの作成

jscodesfhitのドキュメントで指定されているようなディレクトリ構造でfixture fileとテストファイルを作成します。

jscodeshift | Unit Testing

今回は、JavaScriptファイルとTypeScriptファイルのfixture fileを配置します。

/transform.ts
/__tests__/transform.test.js
/__testfixtures__/foobar.input.ts
/__testfixtures__/foobar.output.ts
/__testfixtures__/foobar.input.js
/__testfixtures__/foobar.output.js

__tests__/transform.test.js を以下のように記述しました。
TypeScriptファイルを変換する場合は、parserオプションを指定する必要があるので、defineTest関数で指定しています。

// @ts-ignore
import { defineTest } from "jscodeshift/dist/testUtils";

describe("test with .js file", () => {
  defineTest(__dirname, "transform", null, "foobar");
});

describe("test with .ts file", () => {
  defineTest(__dirname, "transform", null, "foobar", { parser: "ts" });
});

$ npx jest
> jest

 PASS  __tests__/transform.ts
  test with .js file
    transform
      ✓ transforms correctly using "foobar" data (129 ms)
  test with .ts file
    transform
      ✓ transforms correctly using "foobar" data (10 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.14 s

まとめ

jscodeshiftのTransform FileをTypeScriptで書く方法と、TypeScriptファイルでのテストの方法を紹介しました。

ASTベースのプログラミングは、型の恩恵を強く受けることができ、TypeScriptとの相性が非常に良いです。

今後は、jscodeshiftを使う際は、TypeScriptで書くのが良さそうです。

techtrain
プロのエンジニアを目指すU30(30歳以下)の方に現役エンジニアにメンタリングもらえるコミュニティです。
https://techbowl.co.jp/techtrain/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away