Arganoの池田です。 本記事ではTypeScript, JavaScriptプロジェクト内の不要なコードを検知・削除できるKnipというツールの概要と設定について紹介します。

Knip

このツールは冒頭に記載した通りTypeScript, JavaScriptプロジェクト向けの不要コードを検知して削除できるツールです。 例えば、package.jsonに記載があるにも関わらずどこからも使用されていない依存パッケージや、exportしているにも関わらずプロジェクト内でどこからもimportしていないモジュールを検知することが可能です。

https://github.com/webpro-nl/knip

導入〜使用方法

※最新の情報は公式ドキュメントを参照してください。

下記のコマンドでインストールできます。

npm install -D knip

実行コマンドはpackage.jsonのscriptsに以下のように記載した場合は npm run knip で実行可能です。

{
  "name": "my-app",
  "scripts": {
    "knip": "knip"
  }
}

dependencies

実行後、もしプロジェクト内に未使用の依存パッケージが存在する場合は以下のように検知することができます。

my-app % npm run knip

> my-app@0.0.0 knip
> knip

Unused dependencies (1)
uuid  package.json:16:6

--fix オプションで自動修正にも対応しています。knip:fix をpackage.jsonに追加して実行すると、未使用の依存パッケージがpackage.jsonから削除されます。なお、lockファイルの更新は別途必要になります。

{
  "name": "my-app",
  "scripts": {
    "knip": "knip",
    "knip:fix": "knip --fix"
  }
}
my-app % npm run knip:fix

> my-app@0.0.0 knip:fix
> knip --fix

Unused dependencies (1)
uuid  package.json:17:6  (removed)

exports

次に、未使用export(file)の検知を試してみます。プロジェクト内にどこからも使用されていないReactコンポーネント、関数を作成してみます。

import type { ReactNode, MouseEvent } from "react"

interface ButtonProps {
  onClick: (e: MouseEvent<HTMLButtonElement>) => void;
  children?: ReactNode;
}

const Button: React.FC<ButtonProps> = (props) => {
  const { onClick, children } = props;
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
};

export default Button;
function getDate() {
  return new Date();
}

export default getDate;

実行結果は以下のようになります。 .tsx, .ts ともにデフォルトの設定で検知できました。

my-app % npm run knip

> my-app@0.0.0 knip
> knip

Unused files (2)
src/components/Button/index.tsx
src/lib/getDate.ts

設定の変更

もしこのプロジェクトがライブラリである場合は、外部からの利用を想定してこれらのexportを未使用として扱いたくないことがあるかと思います。 そのような場合はKnipの設定ファイルを編集してこれらのファイルを未使用exportの検知から除外する必要があります。

この設定はプロジェクトごとに細かい調整が必要ですが、例えば src/components/*/index.tsx をentryとして認識させて試してみます。

設定ファイルはJSON形式やJavaScriptファイルとして作成することも可能ですが、 今回は knip.ts というファイル名でプロジェクトのルートに配置します。

import { KnipConfig } from "knip";

const config: KnipConfig = {
  entry: ["src/components/*/index.tsx"]
};

export default config;

この設定を追加してから npm run knip を実行してみます。

my-app % npm run knip

> my-app@0.0.0 knip
> knip

Unused files (1)
src/lib/getDate.ts

entryの設定が適用されて、 src/components/Button/index.tsx は未使用とは判定されなくなりました。 entryに追加したファイルは、Knipがプロジェクトのファイルを走査する際の開始地点となります。今回の設定追加によって src/components/Button/index.tsx 自体はプロジェクト内のどこからもインポートされていなくとも未使用とは判定されず、 src/components/Button/index.tsx がインポートしている各モジュールも使用しているものと判定されるようになります。

他のアプローチとして、未使用と判定されたexportを設定から無視することが可能です。

import { KnipConfig } from "knip";

const config: KnipConfig = {
  entry: ["src/components/*/index.tsx"],
  // src/lib 以下のファイルは未使用であっても無視する
  ignoreIssues: {
    "src/lib/**": ["exports", "files"]
  }
};

export default config;

先ほどまで表示されていた src/lib/getDate.ts の未使用の検知を抑えることができました。

 my-app % npm run knip

> my-app@0.0.0 knip
> knip

✂️  Excellent, Knip found no issues.

その他の設定

設定ファイルの調整は上記のようにentryを適切に設定することが主な内容になりますが、それだけだとVueなどのフレームワークを利用している際に未使用exportの走査がうまくいかないことがありました。 公式のドキュメントによるとKnipはTypeScript, JavaScript以外の拡張子のファイルに対してもimport/exportを追跡するためのcompilerという機能を持っており、以下の拡張子のファイルに対してはBuilt-in compilersが存在しています。

Knip has built-in “compilers” for the following file extensions:

  • .astro
  • .css (only enabled by tailwindcss)
  • .mdx
  • .prisma
  • .sass + .scss
  • .svelte
  • .vue

https://knip.dev/features/compilers

私が試した際には上記のVueのBuilt-in compilersでは正確に未使用exportの検知ができなかったため、上記ドキュメントのCustom compilers機能を利用してcompilerの動作を上書きすることで解消しました。上記ドキュメントの設定例を元に一部改修を加えています。

import { KnipConfig } from "knip";
import { compileTemplate, parse, type SFCScriptBlock, SFCStyleBlock, SFCTemplateBlock } from "vue/compiler-sfc";

// デフォルトの設定だと .vue ファイルでimportされているモジュールを正しく探索できていないので設定を変更する

function getScriptBlockContent(block: SFCScriptBlock | null): string[] {
    if (!block) return [];
    if (block.src) return [`import '${block.src}'`];
    return [block.content];
}

function getStyleBlockContent(block: SFCStyleBlock | null): string[] {
    if (!block) return [];
    if (block.src) return [`@import '${block.src}';`];
    return [block.content];
}

function getStyleImports(content: string): string {
    return [...Array.from(content.matchAll(/(?<=@)import[^;]+/g))].join("\n");
}

function getTemplateContent(block: SFCTemplateBlock | null, filename: string): string[] {
    if (!block) return [];

    // VueのテンプレートをJavaScriptのレンダー関数にコンパイルする
    const compiled = compileTemplate({
        source: block.content,
        id: filename,
        filename: filename
    });

    // コンパイルされたJSコードを返す
    // knipで追跡する必要が無いので 'export function render' を 'function render' に置換する
    const codeWithoutExport = compiled.code.replace(/export function render/g, "function render");

    return [codeWithoutExport];
}

const config: KnipConfig = {
    // その他の設定
    // ...
    compilers: {
        vue: (text: string, filename: string) => {
            const { descriptor } = parse(text, { filename, sourceMap: false });

            return [
                ...getScriptBlockContent(descriptor.script),
                ...getScriptBlockContent(descriptor.scriptSetup),
                ...getTemplateContent(descriptor.template, filename),
                ...descriptor.styles.flatMap(getStyleBlockContent).map(getStyleImports)
            ].join("\n");
        }
    }
}

まとめ

実際に使ってみた感想としては、一部プロジェクト毎の設定調整が必要なものの、ほとんどの機能はゼロコンフィグで動作し、コードをクリーンな状態に保つことができるという点でプロジェクトの品質を維持する上で非常に有用なツールだと感じました。是非プロジェクトに導入してみていただければと思います。

参考