こんにちは、taroです!
今回は、ベースマキナのTypeScriptのプロジェクトで不要なコードの検知・削除で使用しているKnipについて紹介します。
Knip とは
Knipは、TypeScript/JavaScriptのコードベースの不要なコードを検出するCLIツールです。
以下が検出できる不要なコードの例です。
package.json
のdependencies
/devDependencies
の中で使われていないpackage- exportされているがどこからもimportされていない変数、関数、型など
- 使用していないファイル
その他、検出できる内容の一覧はこちらで確認できます。
またExperimentalな機能(2024年7月現在)として不要なコードの自動削除も可能です。
ちなみにTypeScript/JavaScriptの不要なコードの検出するツールではts-pruneも知られていますが、ts-pruneは2023年12月にPublic archiveされており、READMEでもKnipの使用が案内されています。
🚨 ts-prune is going into maintanence mode
Please use knip which carries on the spirit.
https://github.com/nadeesha/ts-prune?tab=readme-ov-file#-ts-prune-is-going-into-maintanence-mode
Knipの使い方
Knipの使い方はとてもシンプルです。
対象のプロジェクトにインストールして、ルートディレクトリで実行すると不要なコードを検出できます。
# インストール npm install -D knip typescript @types/node # 実行 npx knip # 出力例 Unused files (1) company.ts Unused devDependencies (1) @testing-library/jest-dom package.json Unlisted dependencies (1) jest-watch-select-projects jest.config.ts Unused exports (1) userIds unknown user.ts:1:14 Unused exported types (1) User type user.ts:2:13
Knipは"zero config"を目指しているツールであり、
- 各種linterやtest runner、ビルドツールの設定で、import文を使わず暗黙的に読み込むpackageがある
- monorepoで複数の
package.json
がある
といった場合でも、ほとんど追加の設定なしで使用できます。
個人的にこういった開発の補助的なツールは、設定が複雑だと導入後にエラーが発生した場合に、対処が放置されがちなのでzero configな点はとてもありがたいです。
ではKnipはどのようにして不要なコードの検出をzero configで実現しているのでしょうか?
Knipが不要なコードを検出する仕組み
Knipが不要なコードを検出する仕組みを理解する上で、重要な概念が以下の2つです。
エントリーファイル
エントリーファイルは、不要なコードを検出する際の起点となるファイルです。
デフォルトではindex.ts
やmain.tsx
などがエントリーファイルです。*1
Knipはエントリーファイルから順番にimportされているファイルを解析していき、使用しているpackageやexportされている変数、関数、型などを検出します。
ファイルの解析後、使用しているpackageをpackage.json
のdependencies
/devDependencies
と比較して、差分がある場合は
unused
(使用していない)unlisted
(使用しているがdependencies
/devDependencies
にない)
なdependencyとして出力します。
Unused devDependencies (1) dayjs package.json Unlisted dependencies (1) date-fns src/utils/date.ts
しかしindex.ts
やmain.tsx
を解析するだけでは検出できないパターンがあります。
そこで登場するのがプラグインです。
プラグイン
プラグインは各ライブラリ(各種linterやtest runner、ビルドツールなど)で使用しているファイルから不要なコードを検出する機能です。
プラグインは主に以下の3つを行います。
それぞれJestのプラグインを例に説明します。
各ライブラリに応じたプラグインの有効化
まずは各ライブラリに応じたプラグインの有効化です。
プラグインの有効化に追加の設定は不要です。
Jestのプラグインでは、package.json
のdependencies
/devDependencies
にjest
があれば自動で有効化されます。*2
各ライブラリに応じたエントリーファイルの追加
次にエントリーファイルの追加です。
Jestのプラグインでは、**/__tests__/**/*.[jt]s?(x)
と**/?(*.)+(spec|test).[jt]s?(x)
がエントリーファイルに追加され、テストファイルのみで使用しているpackageやexportされている変数、関数、型なども検出できるようになります。
各ライブラリの設定ファイル内で使用しているpackageの検出
最後に設定ファイル内で使用しているpackageの検出です。
Jestのプラグインでは、以下の設定ファイルを解析して使用しているpackageを検出します。
jest.config.{js,ts,mjs,cjs,json}
package.json
のjest
の値
例えば、以下の設定ファイルがある場合、Knipはjest-watch-select-projects
を使用しているpackageとして検出します。
// jest.config.ts export default { watchPlugins: ["jest-watch-select-projects"], };
以上のようにKnipでは各種ライブラリを使用している場合でも、プラグインを使用してzero configで不要なコードを検出できるようになっています。
プラグインは多数用意されており、以下が一例です。
プラグインでpackageが正しく検出されない場合の対処方法
Knipはzero configとはいえ、追加設定が必要な場合もあります。
ベースマキナで導入した際は、プラグインに関連したpackageが
- 使用しているのにunusedなdependencyとして検出されてしまう
- 使用していないのにunlistedなdependencyとして検出されてしまう
のように、正しく検出されない場合があったので、最後にその対処方法を紹介します。
正しく検出されないpackageを無視する前に検出できない原因を考える
この場合最初に思いつく方法は、Knipのプラグインの誤検出として正しく検出されないpackageをignoreDependencies
に追加する設定です。
// knip.ts export default { ignoreDependencies: ["jest-watch-select-projects"], };
私も当初はこの方法を取ることが多かったのですが、よくよく調べてみると誤検出ではなく、そもそもプラグインが有効化されてなかったり、ファイルが解析されていないことがほとんどでした。
そのため現在は、無視する前に一度そのpackageが検出できない原因を以下の流れで考えるようにしています。
プラグインが有効化されているか確認する
まずはそのライブラリのプラグインが有効化されているか確認します。
Knipでは--debug
オプションをつけると、実行結果に有効化されたプラグインが表示されます。
npx knip --debug # 出力例 # ... [.] Enabled plugins [ 'ESLint', 'Jest' ] # ...
プラグインが有効化されていない場合
もし有効化されていない場合は、原因を調査してみます。
プラグインが有効化される条件は、ドキュメントの各プラグインのページの"Enabled"に記載されています。
例えばJestのプラグインが有効化される条件は以下のように記載されています。
This plugin is enabled when there’s a match in dependencies or devDependencies in package.json:
- jest
またKnipはTypeScriptで実装されているためソースコードからも比較的簡単にプラグインが有効化される条件を確認できます。
https://github.com/webpro-nl/knip/tree/main/packages/knip/src/plugins
各プラグインはPlugin
interfaceを満たすオブジェクトをexportしており、その中のisEnabled
メソッドで有効化されるかどうかを判定しています。
例えば以下がJestのプラグインのisEnabled
メソッドです。
// https://github.com/webpro-nl/knip/blob/main/packages/knip/src/plugins/jest/index.ts#L11-L14 const enablers = ["jest"]; const isEnabled: IsPluginEnabled = ({ dependencies, manifest }) => hasDependency(dependencies, enablers) || Boolean(manifest.name?.startsWith("jest-presets"));
基本的にプラグインに対応したライブラリがpackage.json
のdependencies
/devDependencies
に含まれているかどうかが、プラグインが有効化される条件ですが、プラグインによってはそれ以外の条件が含まれている場合もあるので、一度ドキュメントやソースコードを確認するのがおすすめです。
プラグインが有効化されているが正しく検出されない場合
次はプラグインが有効化されているが正しく検出されない場合です。
「Knipが不要なコードを検出する仕組み」で述べたように、Knipは主に以下の2箇所からpackageを検出しています。*3
そのため、次はプラグインが追加するエントリーファイルとプラグインに対応したライブラリの設定ファイルを確認してみます。
ドキュメントの各プラグインのページの"Default configuration"の
に記載されています。
例えばJestのプラグインでは以下のように記載されています。
This configuration is added automatically if the plugin is enabled:
{ "jest": { "config": ["jest.config.{js,ts,mjs,cjs,json}", "package.json"], "entry": ["**/**tests**/**/_.[jt]s?(x)", "**/?(_.)+(spec|test).[jt]s?(x)"] } }
https://knip.dev/reference/plugins/jest#default-configuration
有効化の条件と同様に、エントリーファイルと設定ファイルも該当する実装箇所を確認してみます。
エントリーファイルと設定ファイルは、Plugin
のentry
とconfig
に対応しています。
以下がJestのプラグインのentry
とconfig
です。
// https://github.com/webpro-nl/knip/blob/main/packages/knip/src/plugins/jest/index.ts#L16-L18 const entry = ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"]; const config = ["jest.config.{js,ts,mjs,cjs,json}", "package.json"];
entry
とconfig
を確認して、正しく検出されないpackageを使用しているファイルが含まれていない場合は、Knipの設定ファイルに追加します。
// knip.ts export default { jest: { entry: ["**/__custom_tests__/**/*.ts"], config: ["jest.config.ts"], }, };
entry
やconfig
を追加する場合は、デフォルトの値は上書きされるためご注意ください。
どうしても正しく検出されない場合はKnipにコントリビュートするチャンスかも?
もしどうしても正しく検出されない場合は、Knipにコントリビュートするチャンスかもしれません。
ベースマキナのリポジトリに導入した際には、以下の修正を行いました。
不要コードの自動修正で
ignore
やignoreDependencies
などの設定が適用されないバグの修正GraphQL-Codegenのプラグインで、正しく検出できるのが
@graphql-codegen/
で始まるpackageのみだったので、その他のpackageでも検出できるように修正webpackのプラグインで、webpackの設定ファイルの
oneOf
で使用しているpackageのサポート
Knipは実装がTypeScriptで、またプラグインのみであればとてもシンプルなので、比較的理解がしやすいと感じました。
作者のLarsさんもとても親切でPull Requestを送るたびに、Tweetにも反応をもらえてとても嬉しかったです!
3️⃣ You're on fire! 🔥
— Lars Kappert (@webprolific) 2024年5月26日
Thank you so much 🙏❤️
おわりに
今回は、TypeScript/JavaScriptの不要なコードを検出するツール「Knip」について紹介しました。
Knipはzero configを目指しているツールで、簡単に導入できるのでよかったらぜひ使ってみてください。
*1:正確には{index,main,cli}.{js,cjs,mjs,jsx,ts,cts,mts,tsx}、src/{index,main,cli}.{js,cjs,mjs,jsx,ts,cts,mts,tsx}、package.jsonのmain,bin,exportsのファイル、package.jsonのscriptsで指定されているファイルがエントリーファイルとなります。https://knip.dev/explanations/entry-files
*2:正確にはpackage.jsonのnameがjest-presetsで始まる場合にも有効化されます。https://github.com/webpro-nl/knip/blob/main/packages/knip/src/plugins/jest/index.ts#L13-L14
*3:正確にはpackage.jsonのscriptsからも検出しています。https://knip.dev/explanations/entry-files#scripts-in-packagejson