Knipで未使用コード・依存関係を検出 - TypeScriptプロジェクトの最適化ツール
Knipで未使用コード・依存関係を検出
Knipは、TypeScriptおよびJavaScriptプロジェクトの未使用コード、未使用依存関係、到達不可能なコードを検出する強力なツールです。コードベースを整理し、バンドルサイズを削減し、メンテナンス性を向上させるために不可欠なツールです。
Knipとは
Knipは「Knife」の発音に由来し、不要なコードを「切り取る」ことを目的としたツールです。
主な機能
- 未使用ファイルの検出: インポートされていないファイルを発見
- 未使用エクスポートの検出: 使われていないエクスポートを特定
- 未使用依存関係の検出: package.jsonの不要な依存関係を発見
- 到達不可能なコード検出: デッドコードを特定
- 型の未使用検出: TypeScript型定義の未使用を検出
- 循環依存の検出: モジュール間の循環参照を発見
なぜKnipが必要か
プロジェクトが成長するにつれて:
- 使われなくなったコードが蓄積
- 不要な依存関係が残る
- バンドルサイズが肥大化
- メンテナンスコストが増大
Knipが解決:
- 自動的に未使用コードを検出
- 安全に削除可能な部分を特定
- プロジェクトを最適な状態に保つ
インストールとセットアップ
インストール
# npm
npm install -D knip
# yarn
yarn add -D knip
# pnpm
pnpm add -D knip
基本設定
Knipは多くのプロジェクト構成を自動検出しますが、カスタマイズも可能です。
// knip.json
{
"entry": ["src/index.ts", "src/cli.ts"],
"project": ["src/**/*.ts"],
"ignore": ["**/*.test.ts", "**/__tests__/**"],
"ignoreDependencies": ["@types/*"]
}
package.jsonスクリプト
{
"scripts": {
"knip": "knip",
"knip:fix": "knip --fix",
"knip:production": "knip --production"
}
}
基本的な使い方
プロジェクト全体をスキャン
# デフォルトスキャン
npx knip
# 詳細な出力
npx knip --debug
# JSON形式で出力
npx knip --reporter json > knip-report.json
出力例
Finding unused files, dependencies and exports...
Unused files (3)
src/utils/old-helper.ts
src/components/DeprecatedButton.tsx
src/lib/legacy-api.ts
Unused dependencies (2)
lodash
moment
Unused exports (5)
src/utils/helpers.ts: unusedHelper
src/components/Button.tsx: InternalProps
src/lib/api.ts: legacyFetch
Unused types (2)
src/types/index.ts: OldType
src/types/api.ts: DeprecatedResponse
Next.jsプロジェクトでの使用
設定
// knip.json
{
"entry": [
"next.config.{js,ts}",
"app/**/*.{ts,tsx}",
"pages/**/*.{ts,tsx}"
],
"project": ["**/*.{js,ts,jsx,tsx}"],
"ignore": [
".next/**",
"out/**",
"public/**",
"**/*.test.{ts,tsx}",
"**/__tests__/**"
],
"next": {
"entry": [
"app/layout.tsx",
"app/page.tsx",
"app/**/page.tsx",
"app/**/layout.tsx",
"app/api/**/route.ts"
]
}
}
App Routerでの検出
// app/page.tsx
import { UnusedComponent } from '@/components/UnusedComponent'; // Knipが検出
import { UsedComponent } from '@/components/UsedComponent';
export default function Page() {
return <UsedComponent />; // UnusedComponentは使われていない
}
# Knip実行結果
Unused imports (1)
app/page.tsx: UnusedComponent from @/components/UnusedComponent
Reactプロジェクトでの使用
未使用コンポーネントの検出
// src/components/Button.tsx
export function Button() { /* ... */ }
export function InternalButton() { /* ... */ } // 内部でのみ使用予定だが未使用
// Knipが検出
export function UnusedButton() { /* ... */ } // 完全に未使用
# Knip実行結果
Unused exports (2)
src/components/Button.tsx: InternalButton
src/components/Button.tsx: UnusedButton
動的インポートへの対応
// 動的インポート(Knipは検出可能)
const LazyComponent = lazy(() => import('./LazyComponent'));
// 文字列ベースの動的インポート(Knipが検出できない場合)
const componentName = 'DynamicComponent';
const Component = lazy(() => import(`./components/${componentName}`));
// 動的インポートパターンを設定で指定
{
"entry": ["src/components/**/*.tsx"],
"ignoreDependencies": []
}
モノレポでの使用
ワークスペース設定
// knip.json(ルート)
{
"workspaces": {
"packages/ui": {
"entry": "src/index.ts",
"project": "src/**/*.ts"
},
"packages/utils": {
"entry": "src/index.ts",
"project": "src/**/*.ts"
},
"apps/web": {
"entry": ["src/main.tsx", "vite.config.ts"],
"project": "src/**/*.tsx"
}
}
}
ワークスペース間の依存関係
// packages/ui/src/index.ts
export { Button } from './Button';
export { Input } from './Input';
export { UnusedCard } from './Card'; // 他のワークスペースで未使用
// apps/web/src/App.tsx
import { Button, Input } from '@workspace/ui'; // Cardは使われていない
# 特定のワークスペースをスキャン
npx knip --workspace packages/ui
# すべてのワークスペースをスキャン
npx knip --workspace-root
高度な設定
プラグインシステム
Knipは様々なツールやフレームワークのプラグインを提供します。
{
"eslint": true,
"jest": true,
"vitest": true,
"playwright": true,
"webpack": true,
"vite": true
}
カスタムエントリーポイント
{
"entry": [
"src/index.ts",
"src/cli.ts",
"scripts/**/*.ts",
"!scripts/internal/**"
],
"project": [
"src/**/*.ts",
"scripts/**/*.ts"
]
}
依存関係の除外
{
"ignoreDependencies": [
"@types/*", // すべての型定義
"eslint-*", // ESLintプラグイン
"vite", // 開発ツール
"typescript" // TypeScriptコンパイラ
],
"ignoreBinaries": [
"tsc",
"eslint"
]
}
CI/CDへの統合
GitHub Actionsワークフロー
# .github/workflows/knip.yml
name: Knip Analysis
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
knip:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run Knip
run: npm run knip
- name: Upload Knip report
if: failure()
uses: actions/upload-artifact@v4
with:
name: knip-report
path: knip-report.json
pre-commitフック
// package.json
{
"husky": {
"hooks": {
"pre-commit": "knip --no-progress"
}
}
}
または、lint-stagedと組み合わせて。
{
"lint-staged": {
"*.{ts,tsx}": [
"knip",
"eslint --fix"
]
}
}
レポーターのカスタマイズ
JSONレポーター
npx knip --reporter json > knip-report.json
{
"files": [
"src/utils/old-helper.ts",
"src/components/DeprecatedButton.tsx"
],
"dependencies": [
"lodash",
"moment"
],
"exports": [
{
"file": "src/utils/helpers.ts",
"symbol": "unusedHelper"
}
]
}
カスタムレポーター
// reporters/custom-reporter.ts
import type { Reporter } from 'knip';
const customReporter: Reporter = (issues) => {
console.log('🔍 Knip Analysis Report\n');
if (issues.files.length > 0) {
console.log('📁 Unused Files:');
issues.files.forEach(file => console.log(` - ${file}`));
}
if (issues.dependencies.length > 0) {
console.log('\n📦 Unused Dependencies:');
issues.dependencies.forEach(dep => console.log(` - ${dep}`));
}
if (issues.exports.length > 0) {
console.log('\n📤 Unused Exports:');
issues.exports.forEach(exp =>
console.log(` - ${exp.file}:${exp.symbol}`)
);
}
};
export default customReporter;
// knip.json
{
"reporters": ["./reporters/custom-reporter.ts"]
}
パフォーマンスの最適化
キャッシュの活用
# キャッシュを使用してスキャン
npx knip --cache
# キャッシュをクリア
npx knip --cache-clear
インクリメンタルスキャン
# 変更されたファイルのみをスキャン
npx knip --incremental
# 特定のファイルパターンのみ
npx knip --include "src/components/**"
実践例: 大規模プロジェクトのクリーンアップ
ステップ1: 初回スキャン
# まずは現状を把握
npx knip --reporter json > knip-initial.json
# 統計を確認
cat knip-initial.json | jq '{
files: .files | length,
dependencies: .dependencies | length,
exports: .exports | length
}'
ステップ2: 段階的なクリーンアップ
# 1. 未使用ファイルから削除
npx knip --include-files
# 2. 未使用依存関係を削除
npx knip --include-dependencies
# 3. 未使用エクスポートを修正
npx knip --include-exports
ステップ3: 自動修正
// scripts/cleanup.ts
import { execSync } from 'child_process';
import fs from 'fs/promises';
async function cleanup() {
// Knipレポートを生成
const report = JSON.parse(
execSync('npx knip --reporter json').toString()
);
// 未使用ファイルを削除
for (const file of report.files) {
console.log(`Deleting ${file}`);
await fs.unlink(file);
}
// 未使用依存関係をpackage.jsonから削除
const pkg = JSON.parse(await fs.readFile('package.json', 'utf-8'));
for (const dep of report.dependencies) {
delete pkg.dependencies[dep];
delete pkg.devDependencies[dep];
console.log(`Removed dependency: ${dep}`);
}
await fs.writeFile('package.json', JSON.stringify(pkg, null, 2));
console.log('Cleanup completed! Run npm install to update lock file.');
}
cleanup();
トラブルシューティング
false positiveを回避
{
"ignore": [
// テストファイル
"**/*.test.ts",
"**/__tests__/**",
// 設定ファイル
"*.config.{js,ts}",
// 型定義のみのファイル
"**/*.d.ts"
],
"ignoreExportsUsedInFile": true
}
動的インポートの問題
// 動的インポートを明示的に宣言
// knip:ignore-next-line
const Component = await import('./DynamicComponent');
// または設定で除外
// knip.json
{
"ignoreDynamicImports": true
}
ベストプラクティス
1. 定期的なスキャン
{
"scripts": {
"lint": "eslint . && knip",
"precommit": "lint-staged && knip",
"ci": "npm run lint && npm run test && knip"
}
}
2. 段階的な導入
# 最初は警告のみ
npx knip --warn-only
# 慣れてきたらエラーに
npx knip
3. チーム内での共有
# docs/knip-guide.md
## Knipの使い方
### ローカル開発
- コミット前に `npm run knip` を実行
- 未使用コードを定期的にチェック
### CI/CD
- PRごとに自動実行
- mainブランチは常にクリーンな状態を維持
まとめ
Knipは、TypeScript/JavaScriptプロジェクトの健全性を保つための強力なツールです。
主な利点
- コードベースの最適化: 未使用コードを削除
- バンドルサイズの削減: 不要な依存関係を除去
- メンテナンス性の向上: クリーンなコードベース維持
- 自動化: CI/CDに統合して継続的に監視
導入効果
- バンドルサイズの10-30%削減
- ビルド時間の短縮
- コードレビューの負担軽減
- 技術的負債の削減
次のプロジェクトでKnipを導入し、クリーンで効率的なコードベースを維持してみてください。