STORES 予約 の開発をしている菊池です。このブログでは Knip 導入について書きます。 Knip は JavaScript や TypeScript のプロジェクトのコードをお片付けするためのツールです。 Knip はオランダ語で「ハサミ」という意味のようで、コードを削減する意味ではわかりやすいネーミングです。
ちなみに ts-prune というツールもあったのですが、メンテナンスモードとなり Knip を勧めている状況となっています。 ts-prune は README に書いてある通り、多くのユースケースをサポートする中、複雑性が増し、コアシステムの変更や機能追加が気軽にできなくなったのが背景としてあるようです。
GitHub - nadeesha/ts-prune: Find unused exports in a typescript project. 🛀
背景
Backend では Ruby on Rails、フロントエンドでは一部を除いて Next.js / React / TypeScript などを用いて開発しています。 サービスとして10年以上続いており、このフロントエンドの構成ができたのは2021年2月あたりになります。ブログを執筆した時点までは3年10ヶ月経ちます。 コミットは15000コミットもあり、開発に関わる人数も50人と多いものです( Contributors の数で見ただけで現在進行形では、14,5人です)
開発者が入れ替わるのと複数のプロジェクト、複数のチームで contribute するのもあって「経緯がわからないが残っているコード」はしばしばあり、これは個人的にはいらなかったとしても目につけばそれは認知負荷(課題外在性負荷)になると考えています。 そのため不要だったら消すというのをできるだけやっていきたいと思い、 Knip を入れることにしました。
導入
npm package なので knip のサイトの getting-started を見てシンプルに始められます。
https://knip.dev/overview/getting-started
そこからプロジェクトの個別の設定を knip.json で記述します。一部を取り除いていますが、下記が STORES 予約 での設定です。
{ "entry": ["src/**.ts"], "project": ["src/**"], "ignore": [ "package.json", "babel.config.js", ".github/**", ], "vitest": false, "rules": { "unlisted": "off", // メインの package に含まれてインストールされるものを使うのは意図しているものなので off } }
主に ignore, rules の部分が主な個別の部分ですが、いくつかその理由を説明すると、
- build 時に使用している dependencies まで検知されてしまうため、
package.json
babel.config.js
などを ignore している - Vitest で使用している Plugin の影響で Knip のコマンドが実行できないので、
vitest: false
にしてオフにしている unlisted: off
の部分はコメントがある通りで、例えば Next.js のstyled-jsx/css
を使っていたりすると検知されてしまう
json にコメントが書ける通り、 jsonc でも記述できたりするので、 off にした場合の経緯を記述できて非常に便利です。
普段の開発での活用
導入しただけでは使われないので、定常的な開発の中で活かせるようにします。 GitHub の PR 上でそれに気づき、開発者が消すようにします。
- GitHub に対してのコメントで Knip が出すレポートを確認できる
- レポートを元に開発者が、その要否を判断する
- 不要であれば、エディタを使うもしくは Knip のコマンドのオプションの
--fix
を使ってコードを削除する
--fix
というのは、検知したものを消すオプションなのですが、 PR 上で自動で消すというのはしていません。
開発者の意図しないコミットが積まれたり、「この後使うメソッドなので消されると困る」と言ったことが発生するためです。
PR 上に自動でコメントするために、 GitHub Actions を使います。以下がその yaml です。
name: check-knip.yml on: push: branches: - main pull_request: jobs: build: runs-on: ubuntu-latest timeout-minutes: 25 steps: - uses: actions/checkout@v4 - name: setup node uses: actions/setup-node@v3 with: node-version-file: '.node-version' cache: 'yarn' - name: node modules cache uses: actions/cache@v3 id: node-modules-cache with: path: './node_modules' key: ${{ runner.os }}-build-${{ hashFiles('./.node-version') }}-${{ hashFiles('./yarn.lock') }} - if: ${{ steps.setup-node.outputs.node-modules-cache-hit != 'true' }} name: Install dependencies run: yarn --frozen-lockfile - name: knip id: knip-check run: yarn run knip:check > /tmp/knip-report continue-on-error: true - name: Create PR comment uses: actions/github-script@v6 if: steps.knip-check.outcome == 'failure' with: github-token: ${{secrets.GITHUB_TOKEN}} script: | const fs = require('fs') const knip_report = fs.readFileSync('/tmp/knip-report', 'utf8') const output = ` Knip により検知されました。下記の詳細から確認して、不要であれば対応してください。 なお \`yarn knip:fix\` というコマンドも使えます。 <details><summary>Knip Report📖</summary> ${knip_report} </details>`; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: output })
「 CI が error で止まると、開発中事前に準備しようとしたメソッドなども検知されて開発が進まなくなる」というフィードバックをチーム内でもらったので、 continue-on-error: true
を入れています。
yarn knip:fix
というのは knip --fix --allow-remove-files
が実体のコマンドです。 --fix
は上述した通りのオプションですが、 --allow-remove-files
はファイル自体が不要であれば削除するオプションです。
Knip を実行してみる
GitHub 上では、差分だけでなくプロジェクト全体に対する検知がされるので、現状の不要なものを削除してみます。 GitHub Actions を追加される前に適用することで、最初から検知されるのを防ぎます。
3400行ほどのコードを消すことができました。
おわりに
Knip を普段から使うための内容を記述してきました。まだ一時的に ignore しているファイルや検知してほしくない対象などが検知されてしまうことがあるので設定などは今後も改善の余地はありそうですが、大幅にコードを減らせ今後も開発に役立てそうです。
設定ファイルなどは単にコピペできる内容ではありませんが、ぜひ参考にしてみてください。