弁護士ドットコム株式会社 Creators’ blog

弁護士ドットコムがエンジニア・デザイナーのサービス開発事例やデザイン活動を発信する公式ブログです。

年末なのでコードの大掃除をしよう

ブログタイトル画像「年末なのでコードの大掃除をしよう」と記載。右下にクラウドサインマスコットキャラクター「カプラ」(動物のやぎ)が写っている。この記事は弁護士ドットコム Advent Calendar 2024の 8 日目の記事です。

クラウドサイン事業本部でエンジニアをやっている田邉です。

みなさんの js、ts プロジェクトには、なぜ export しているのかわからないコードや、なんのためにインストールしているのかわからないモジュールはありませんか?
それらが不要だとしたらこの年末に大掃除するチャンスです!
※ 本記事の内容は既存コードに対する大幅な変更が発生する可能性があります。タイトルには反しますが、年末に実施する場合は十分に注意して実行してください。

私の関わっている事業のプロジェクトでは Next.js を利用しているフロントエンドで以下のような課題を抱えていました。

  • 使われていないコンポーネントや hook、メソッドや type が溜まってしまう
  • 不要な export が発生してしまう
  • vanilla-extract を利用しており、スタイルを別ファイルに記載する方法をとっていることで使われていないスタイリングが発生しやすい

これらを解消するために knip を利用することを決めました。
本ドキュメントでは knip について説明し、実際にどう利用したのかを紹介いたします。

knip の仕組み

knip では entry に設定されたファイルを起点に、import しているモジュール(ファイル)を辿ってツリーに組み込まれたファイルを resolved file としてマークしていきます。

knip で検出されたファイルツリーの一部を表示させてみると以下のようになりました。

apps/・・・/CreatedAgendasListModal/CreatedAgendasListModalCard.tsx:CreatedAgendasCard
└─ apps/・・・/CreatedAgendasListModal/CreatedAgendasListModal.tsx ✓

apps/・・・/CreatedAgendasListModal/CreatedAgendasListModal.tsx:CreatedAgendasListModal
└─ apps/・・・/CreatedAgendasListModal/index.ts
   └─ apps/・・・/MeetingHeader/MeetingHeader.tsx ✓

apps/・・・/CreatedAgendasListModal/CreatedAgendasListModalCard.css.ts:createdAgendasCard
└─ apps/・・・/CreatedAgendasListModal/CreatedAgendasListModalCard.tsx ✓

上記の例からは

  • CreatedAgendasListModalCard.tsx に含まれる CreatedAgendasCard というメソッドが CreatedAgendasListModal.tsx というファイルから呼び出されている
  • CreatedAgendasListModal.tsx に含まれる CreatedAgendasListModal というメソッドが CreatedAgendasListModal/index.ts を経由して MeetingHeader.tsx から呼び出されている
  • ・・・

というツリー構成を読み取ることができます。

knip では、このようにして検出したファイルを resolved としてマークしていき、最終的にどこでも利用されていないファイルを以下のような式で検出するようです。

unused files = project files - (entry files + resolved files)

https://knip.dev/guides/configuring-project-files

knip の使い方

検出できるもの

knip が検出するのは不使用のファイルだけではなく、未使用のモジュールなども含まれます。
以下に 2024 年 12 月時点で knip で検出できるものの一部を列挙します。

  • 使用されていないファイル
  • dependencies に含まれるにも関わらず使用されていないモジュール
  • devDependencies に含まれるにも関わらず使用されていないモジュール
  • 参照されていない peerDependencies
  • コード内で利用されているにも関わらず package.json に記載されていない(明示的にインストールされていない)モジュール
  • import されるモジュールが見つからない
  • 不必要な export や export type
  • export された enum や class の中で使用されていないメンバー

https://knip.dev/reference/issue-types

基本的な使い方

では具体的にどのようにしてプロジェクトで利用するかを説明します。

公式サイトに Knip has good defaults and aims for “zero config”. Here’s a simplified version of the default configuration とあるように、knip は zero config を目指しており、基本的には設定ファイルなしで利用できます。
これは knip の plugin という仕組みのおかげで、たとえば package.json の dependency もしくは devDependency に next を発見すると、knip は自動で以下のような設定を追加します。

{
  "next": {
    "entry": [
      "next.config.{js,ts,cjs,mjs}",
      "{instrumentation,middleware}.{js,ts}",
      "app/global-error.{js,jsx,ts,tsx}",
      "app/**/{error,layout,loading,not-found,page,template,default}.{js,jsx,ts,tsx}",
      "app/**/route.{js,jsx,ts,tsx}",
      "app/{manifest,sitemap,robots}.{js,ts}",
      "app/**/{icon,apple-icon}.{js,jsx,ts,tsx}",
      "app/**/{opengraph,twitter}-image.{js,jsx,ts,tsx}",
      "pages/**/*.{js,jsx,ts,tsx}",
      "src/{instrumentation,middleware}.{js,ts}",
      "src/app/global-error.{js,jsx,ts,tsx}",
      "src/app/**/{error,layout,loading,not-found,page,template,default}.{js,jsx,ts,tsx}",
      "src/app/**/route.{js,jsx,ts,tsx}",
      "src/app/{manifest,sitemap,robots}.{js,ts}",
      "src/app/**/{icon,apple-icon}.{js,jsx,ts,tsx}",
      "src/app/**/{opengraph,twitter}-image.{js,jsx,ts,tsx}",
      "src/pages/**/*.{js,jsx,ts,tsx}"
    ]
  }
}

こうした plugin を他にも多数揃えているおかげで、基本的には設定ファイルなしでの利用が可能となっています。

ただし、特定のディレクトリでは knip によるチェックを回避したいケースや、後で記載するように ESLint などのライブラリの設定に特殊なファイルを利用するケースなど、設定ファイルを要するシーンは少なくない印象です。

以下に示す例では

  • 特定のワークスペースで index.js を ESLint の設定ファイルとして認識させる
  • 特定の依存関係における検出を無視する

といった設定をしています。

import type { KnipConfig } from 'knip'

const config: KnipConfig = {
  ignoreDependencies: ['lerna'],
  workspaces: {
    'packages/eslint-config': {
      eslint: {
        config: 'index.js',
      },
    },
  },
}
export default config

設定ファイルは json や ts など、多くの形式をサポートしています。 詳しくは以下を参照してください。

https://knip.dev/overview/configuration#location

設定ファイルの記法については以下を参照してください。

https://knip.dev/guides/configuring-project-files

それでは実際に実行した際にどのように結果が表示されるか、以下に例を示します。
なお実際のプロジェクトで試した結果ではないのでご注意ください。

% yarn knip
Unused files (1)
workers/src/tracer.ts
Unused dependencies (2)
pino-pretty            apps/bff/package.json
yjs                    apps/app/package.json
Unused devDependencies (1)
ts-loader                          apps/bff/package.json
Unlisted dependencies (1)
fastify                       apps/bff/src/app.module.ts
Unused exported types (1)
Task                            type  workers/src/database/types.ts:92:13
Unused exported enum members (3)
UNSPECIFIED          AgendaStatus         apps/bff/src/modules/agenda/agenda.model.ts:112:3
OPEN                 AgendaStatus         apps/bff/src/modules/agenda/agenda.model.ts:113:3
CLOSED               AgendaStatus         apps/bff/src/modules/agenda/agenda.model.ts:114:3

公式にも記載されているように package.json の scripts に knip コマンドを設定しておきましょう。
私のプロジェクトでは yarn を利用していたので、上記の例のように yarn knip コマンドで実行できました。

  "scripts": {
    "knip": "knip",
    // ・・・
  }

CLI での実行

検出する対象については CLI のフラグを利用することでも制御できます。

https://knip.dev/reference/cli#--include

たとえば使用していないファイルと dependency のみ検出したい場合は以下のコマンドを実行します。

knip --include files,dependencies

検出するタイプを表すフラグは以下を参照してください。

https://knip.dev/reference/issue-types

プロジェクトでの knip 運用

本章では実際のプロジェクトで、どのように運用してきたかを説明します。

内容説明の前にプロジェクトについて以下のような特徴があることを共有しておきます。

  • yarn workspace を利用したモノレポ環境であること
  • リポジトリには GitLab を利用しており、CI でフロントエンドの自動テストなどを実行していること

workspace 下で利用する

まず考慮しなければならないのは workspace を利用していることです。
yarn workspace などを利用中の場合、knip をワークスペースのルートでインストールし、設定ファイルに以下のように記載します。

{
  "workspaces": {
    "apps/app": {
      "ignore": ["src/graphql/generated/**", "public/**", "src/mock/**"],
      "ignoreDependencies": [
        // ・・・
      ]
    }
  },
  // ・・・
}

上記のように記載することでワークスペースごとに enrty や ignore などの設定などが可能となります。

また knip をワークスペースのルートで実行するのではなく、たとえば apps/app で scripts 定義し、実行した場合を考えます。

  "scripts": {
    "knip": "knip"
  }

この場合、knip 自体が Unlisted binaries としてマークされるはずです。
なぜかというと、knip コマンドを実行するためのバイナリは knip パッケージに含まれますが、knip パッケージ自体はワークスペースルートの node_modules に含まれるため、apps/app の package.json から見ると同一階層の node_modules にはないと判断してしまうためです。

これは knip パッケージだけの問題ではなく、たとえば ESLint の設定ファイルなどに含まれる eslint-config-prettier なども Unused dependencies としてマークされるでしょう。

したがって、ワークスペースを利用している場合はルートで knip を実行するように、上記のような設定を施しておくことをおすすめします。

auto-fix を利用する

knip では --fix というフラグをつけることで問題点の自動修正をしてくれます。

https://knip.dev/features/auto-fix#flags

さらに --allow-remove-files フラグをつけることで不使用とマークされたファイルの自動削除までやってくれます。

私のプロジェクトでは package.json の scripts に以下の 2 つのコマンドを用意しました。

  "scripts": {
    "knip": "knip",
    "knip:fix": "knip --fix --allow-remove-files"
  }

CI に組み込む

knip によるチェックを CI で実行することで、不要なファイルや無駄な export が発生した時点でそれに気づくことができます。
弊社では GitLab を利用しているため、.gitlab-ci.yml にワークスペースルートの scripts に定義した knip コマンドを実行するような処理を記載しました。

production モードをうまく使う

production モードでは実行時にテストコードや storybook など、本番環境で利用しないファイルは除外されます。
これにより、通常実行では検出されなかったテストコードのための export などはエラーとして検出されるようになります。

production モードの具体的な挙動は以下のとおりです。

Only entry and project patterns suffixed with ! Only production entry file patterns exported by plugins (such as Next.js and Remix) Only the start and postinstall scripts Ignore exports with the @internal tag

https://knip.dev/features/production-mode

テストコードのためだけの export は本番環境では無駄な export になってしまうので、types.ts などの別ファイルに切り出すか、本当にそのコンポーネントでのみ利用するような場合は二重定義にはなってしまいますがテストコードでも同じ定義を持つようにするなどしたほうが良いでしょう。

こうした気づきを得るためにも、scripts に定義するのはアリだと思います。

一方で、たとえば Next.js プロジェクトでは next.config.js でのみ利用されるモジュールは Unused としてマークされてしまうことに注意です。

Unused dependencies (2)
@vanilla-extract/next-plugin  package.json
next-videos                   package.json
const { createVanillaExtractPlugin } = require('@vanilla-extract/next-plugin')
const withVanillaExtract = createVanillaExtractPlugin()
const withVideos = require('next-videos')

/** @type {import('next').NextConfig} */
const nextConfig = {・・・}

module.exports = withVanillaExtract(withVideos(nextConfig))

ちょっと詰まったとこ

プロジェクトでは graphql-codegen-typescript-mock-data というライブラリを利用して、GraphQL スキーマからテストデータを自動生成できるようにしているのですが、GraphQL Codegen の設定ファイルである codegen.ts に以下のように記載していると unused としてマークされてしまいました。

    './src/graphql/generated/mock.ts': {
      plugins: [
        {
          'typescript-mock-data': {
            typesFile: './graphql.ts',
            terminateCircularRelationships: true,
            prefix: 'createMock',
            enumValues: 'upper-case#upperCase',
          },
        },
        {
          add: {
            content: '/* eslint-disable */',
          },
        },
      ],
    },
Unused devDependencies (1)
graphql-codegen-typescript-mock-data  apps/app/package.json
Unlisted dependencies (1)
@graphql-codegen/typescript-mock-data  apps/app/codegen.ts

GraphQL Codegen は knip のプラグインにあり、graphql-codegen-typescript-mock-data も GraphQL Codegen の公式プラグインに含まれるので不思議だったのですが、コードを見ると codegen- という文字列を含んでいなければならないようだということがわかりました。

https://github.com/webpro-nl/knip/blob/main/packages/knip/src/plugins/graphql-codegen/index.ts

そこでプラグインの指定方法を以下のようにフルネーム指定してみたところ、knip に認識されるようになり、問題を解消できました。

    './src/graphql/generated/mock.ts': {
      plugins: [
        {
          'graphql-codegen-typescript-mock-data': {

他にも

冒頭で表示したような knip が検出したツリー構成の可視化をしたければ trace フラグを利用できます。

https://knip.dev/guides/troubleshooting#trace

ワークスペースがちゃんと指定できているか、プラグインの検出が問題ないかなど、knip の報告の理由を知りたければ debug フラグを利用すると良いでしょう。

https://knip.dev/guides/troubleshooting#debug

またプロジェクトでは利用しませんでしたが、レポーター機能などもあるようなので気になる方は調べてみてください。

まとめ

たくさん書いてきましたが、私の携わっているプロジェクトでは knip はフロントエンドの無駄なコード検出にはなくてはならない存在になりました。

導入したいと思ってくれる方がいれば幸いですが、年末ですので auto-fix する際はくれぐれも慎重に、そして自己責任でお願いします。

良いお年を!