🗑️

Next.jsで使われていないコードをts-pruneで一掃し、CIで仕組み化する

2023/12/22に公開

今回はゲームエイトのNext.jsが使われているプロダクトにて、ts-pruneを用いて一切参照されていないデッドコードを全て洗い出し、削除してみました。
また、これ以降はデッドコードが発生しないようにGithub Actionsに組み込み、デッドコードが生まれたらCIがコケる仕組みを導入しました。

デッドコードになりがちなのは汎用的なUIパーツ

eslintではファイル単位で未使用の変数(コンポーネント、関数)を検知するno-unused-varなどがありますが、このルールは該当の変数がexportされていると、実際にはどこからも参照されていない場合でも、検知されなくなってしまいます。そのため、汎用的なUIパーツなど各ページから参照されるものは、そのパーツがどのページからも使われなくなった時に、削除するべきなのに見過ごしがちになる問題がありました。

同じ悩みをもつissueを見つけ、いくつかの代替案が書かれていましたが、その中でも簡単そうなts-pruneを導入してみました。

ts-pruneのインストールと実行

ドキュメントそのままですが、インストールします。

# npm
npm install ts-prune --save-dev
# yarn
yarn add -D ts-prune

package.jsonにスクリプトを登録します。

{
  "scripts": {
    "find-deadcode": "ts-prune"
  }
}

実行してみます。

> yarn find-deadcode

yarn run v1.22.19
$ ts-prune
src/lib/$path.ts:187 - PagesPath
src/lib/$path.ts:605 - StaticPath
src/pages/_app.tsx:79 - default
src/pages/404.tsx:12 - default
src/pages/500.tsx:12 - default
src/pages/index.tsx:58 - getServerSideProps
(中略)
Done in 6.35s.

検知されたファイルのパスと行番号・関数名が出ますので、不要か検証して個別に修正していきます。自動修正オプションがあるものかと期待したのですが、Next.jsなどフレームワークの振る舞いによってgetServerSidePropsなど参照されなくても実際は必要なexportがあるので、自動修正は現実的ではないのでしょう。詳しくは後述します。

実情に合わせてts-pruneの出力を整える

オプションやgrepで出力を絞らない、生の出力のままだと、対応すべきでないファイルも多く検出されてしまいます。
1度だけの実行であれば出力を1行ずつ判断して対応不要ならスキップするだけでいいのですが、CIに組み込むことも視野に入れ、変更すべきファイルのみが出力されるようにしてみました。

先に結論ですが、該当のプロダクトの状況に合わせて調整された最終的なts-pruneの実行コードは以下のようになりました。

yarn find-deadcode --ignore 'src/lib/\$path.ts|src/types/api/models/landingPageContent/content.ts' | \
  grep -v -E "src/(app|pages)/.* - (default|GET|dynamic)" | \
  grep -v getServerSideProps

理由は以下のとおりです。

  • 自動生成されているファイルは// ts-prune-ignore-nextを手動で付与してignoreせず、--ignoreオプションでコマンドからファイル名を指定しました。(自動生成されるファイルは再出力で手動修正が消えてしまい手動修正を維持しにくい環境のため)
    • pathpidaを使っているため、自動生成ファイル(src/lib/$path.ts)をignoreしました
    • quicktypeを使っているため、自動生成ファイル(src/types/api/models/landingPageContent/content.ts)をignoreしました
  • Next.jsの仕様に基づいた修正
    • 関数getServerSidePropsは参照されてなくてもexportしていないといけない、かつファイルが増えるたびに// ts-prune-ignore-nextを付与するのは疲れるためgrepで除きました。
    • 同じ理由でページを司るpages、app配下のdefaultや、APIのとしてdynamic, GETなど特定の関数もgrep正規表現で出力から取り除きました

結果として// ts-prune-ignore-nextを付与したのはnext.config.js(.mjs)にのみになりました。ページ等と違ってファイルが増えることが予想されない、かつ自動生成でなく手動の編集が容易な類のファイルであれば、個別対応が簡潔で良いと思います。

そして無事に出力結果で残ったものは全て修正(≒削除)することができました。

Github Actionsに組み込んで仕組み化

次に↑のコードを、CIに以下のように組み込みました。

jobs:
  ts-prune:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: ts-prune(デッドコードの除去)
        shell: bash
        run: |
          yarn find-deadcode --ignore 'src/lib/\$path.ts|src/types/api/models/landingPageContent/content.ts' | \
            grep -v -E "src/(app|pages)/.* - (default|GET|dynamic)" | \
            grep -v getServerSideProps | \
            (! grep -v -E "(ts-prune --ignore|Done in |yarn run)")

追加した点としては検知したらCIがコケるようエラーを吐かせるようにしたかったので、grepが1行も出力できなければエラー終了する挙動を活用しようと思いました。

そのため、前のコマンドに加え、ファイル一覧以外に表示される”Done in ◯.◯◯s.”等の余計な出力を除き、出力する行が0なら正常終了したいので!で囲みそのgrepの終了コードを反転させました。

これにて意図通り何も検知しなければCIがパスし、検知するとCIがコケて記事冒頭の画像のよう対象の情報を表示するようになりました。

以上です。年末も近いので、皆さんのプロダクトでも大掃除するのはいかがでしょうか。

ゲームエイトテックブログ

Discussion