# 機能設計書 64-CleanPlugin

## 概要

本ドキュメントは、webpackの組込みプラグインである `CleanPlugin` の機能設計を記述する。`CleanPlugin` は、ビルド前に出力ディレクトリをクリーンアップするプラグインである。

### 本機能の処理概要

CleanPluginは、ビルド出力時に、現在のコンパイルで生成されるアセットに含まれないファイル・ディレクトリを出力ディレクトリから削除する機能を提供する。初回ビルドではファイルシステムを走査して不要ファイルを検出し、インクリメンタルビルドではメモリ上の前回アセットリストとの差分で効率的に削除対象を特定する。

**業務上の目的・背景**：webpackのビルドを繰り返すと、ファイル名にハッシュを含む場合に古いアセットファイルが出力ディレクトリに蓄積される。CleanPluginはこの問題を解消し、出力ディレクトリを常に最新のビルド成果物のみが含まれるクリーンな状態に保つ。

**機能の利用シーン**：本番ビルド時の出力ディレクトリクリーンアップ、watchモードでのインクリメンタルビルド時の古いアセット削除、HMRアセットの一時保持（10秒間）に利用される。

**主要な処理内容**：
1. `emit` フック（stage: 100）で現在のコンパイルアセット一覧を構築する
2. 初回は `getDiffToFs()` でファイルシステムを走査し、現在のアセットに含まれないファイルを検出する
3. 2回目以降は `getDiffToOldAssets()` でメモリ上の前回アセットリストとの差分を計算する
4. `applyDiff()` で差分ファイルの削除（unlink）またはディレクトリの削除（rmdir）を実行する
5. `keep` オプションまたは `CleanPlugin.getCompilationHooks(compilation).keep` フックで保持対象を指定できる
6. `dry` モードでは実際の削除を行わず、ログ出力のみ行う
7. HMRアセットには10秒間のタイムスタンプを設定し、即座に削除されないようにする

**関連システム・外部連携**：OutputFileSystemのreaddir/stat/unlink/rmdir操作、Compilationのアセット管理、processAsyncTreeユーティリティと連携する。

**権限による制御**：権限による制御は存在しない。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | - | - | 本機能に直接関連する画面はない |

## 機能種別

データ連携 / ファイル操作（出力ディレクトリクリーンアップ）

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| dry | `boolean` | No | trueの場合、実際の削除を行わずログのみ出力 | デフォルト: false |
| keep | `string \| RegExp \| ((path: string) => boolean)` | No | 保持対象パターン。文字列はプレフィックスマッチ、RegExpはtestメソッド、関数はカスタム判定 | - |

### 入力データソース

- compilation.assetsオブジェクト（現在のアセット一覧）
- OutputFileSystem（出力ディレクトリのファイル一覧）
- メモリ上の前回アセットリスト（oldAssets）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| 削除ログ | `string` | 削除されたファイル・ディレクトリのログメッセージ |
| keptAssets | `Map<string, number>` | 保持されたアセットの一覧 |

### 出力先

OutputFileSystem（ファイル・ディレクトリの削除）、コンパイルログ出力。

## 処理フロー

### 処理シーケンス

```
1. emitフック（stage: 100）でトリガー
   └─ 通常のemitより後に実行される
2. 現在のアセット一覧を構築
   └─ compilation.assetsからファイル名を正規化し、HMRアセットには10秒のタイムスタンプを設定
3. 差分の計算
   └─ 初回: getDiffToFs()でファイルシステムを走査
   └─ 2回目以降: getDiffToOldAssets()でメモリ上の差分計算
4. applyDiff()で差分を処理
   └─ 各ファイルについてcheck → unlink/rmdir の順でジョブを処理
5. keep判定
   └─ CleanPluginフックまたはkeepオプションで保持対象を判定
6. 結果の保存
   └─ oldAssetsを更新し、keptAssetsをマージ
```

### フローチャート

```mermaid
flowchart TD
    A[emit フック stage:100] --> B[現在のアセット一覧構築]
    B --> C{oldAssets が存在?}
    C -->|No| D[getDiffToFs - FS走査]
    C -->|Yes| E[getDiffToOldAssets - メモリ差分]
    D --> F[applyDiff]
    E --> F
    F --> G{各ファイルについて}
    G --> H{isKept?}
    H -->|Yes| I[保持してログ出力]
    H -->|No| J{ディレクトリ?}
    J -->|Yes| K[子要素を再帰チェック後rmdir]
    J -->|No| L[unlink で削除]
    K --> M[oldAssets を更新]
    L --> M
    I --> M
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-01 | HMR保護 | HMRアセットは現在時刻+10秒のタイムスタンプで保護される | assetInfo.hotModuleReplacementがtrueの場合 |
| BR-02 | ドットファイル保護 | ドットで始まるファイルは削除されない | ファイル名が"."で始まる場合 |
| BR-03 | 絶対パス除外 | 絶対パスのアセットはクリーニング対象外 | パスが"/"、"\\\\", "X:"で始まる場合 |
| BR-04 | 親ディレクトリ除外 | "../"で始まるパスはクリーニング対象外 | 正規化後のパスが"../"で始まる場合 |
| BR-05 | インクリメンタル最適化 | 2回目以降はメモリ上の差分のみで計算する | oldAssetsが存在する場合 |
| BR-06 | タイムスタンプ保護 | oldAssetsのタイムスタンプが現在時刻以上のアセットは削除しない | ts >= now の場合 |

### 計算ロジック

差分計算: `getDiffToOldAssets()` は、前回アセットに存在して現在アセットに存在しないもの（かつタイムスタンプが現在時刻より前）を差分として返す。

## データベース操作仕様

### 操作別データベース影響一覧

データベース操作は存在しない。ファイルシステム操作（readdir, stat/lstat, unlink, rmdir）のみ。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| ENOENT | ファイル不在 | 削除対象が既に存在しない場合 | 既に削除済みとしてスキップ |
| ENOTDIR | 非ディレクトリ | readdirの対象がディレクトリでない場合 | diffに追加して削除対象とする |
| EPERM | 権限エラー | ファイル削除の権限がない場合 | ファイルが存在しなければスキップ、存在すればエラー |
| readdir未サポート | FS制限 | OutputFileSystemがreaddirをサポートしない場合 | エラーとしてコールバック |
| unlink未サポート | FS制限 | OutputFileSystemがunlinkをサポートしない場合 | 警告を出力してスキップ |
| rmdir未サポート | FS制限 | OutputFileSystemがrmdirをサポートしない場合 | 警告を出力してスキップ |

### リトライ仕様

リトライ仕様は存在しない。ENOENT/EPERMエラーは既に削除されたものとして処理をスキップする。

## トランザクション仕様

トランザクション仕様は存在しない。ファイル削除は個別に実行され、部分的な失敗が発生する可能性がある。

## パフォーマンス要件

- 並列度10で非同期ファイル操作を実行する（asyncLib.forEachLimit、processAsyncTree）
- 2回目以降のビルドではファイルシステム走査を省略し、メモリ上の差分のみで計算する

## セキュリティ考慮事項

- 出力ディレクトリ外のファイルは削除されない（絶対パスおよび"../"パスの除外）
- dryモードにより、実際の削除前に影響範囲を確認できる

## 備考

- `CleanPlugin.getCompilationHooks(compilation).keep` フックを使用すると、他のプラグインから保持ルールを動的に追加できる
- emitフックのstage:100は、通常のアセット出力（stage未指定=0）より後に実行されることを意味する
- `_getDirectories` がテスト用にexportされている

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | CleanPlugin.js | `lib/CleanPlugin.js` | `Assets` 型（23行目）: `Map<string, number>` - ファイル名とタイムスタンプのマップ |
| 1-2 | CleanPlugin.js | `lib/CleanPlugin.js` | `CleanPluginCompilationHooks` 型（26-28行目）: `keep` フックの定義 |
| 1-3 | CleanPlugin.js | `lib/CleanPlugin.js` | `Job` 型（193行目）: check/unlink/rmdirの3種のジョブタイプ |

**読解のコツ**: ファイル名のタイムスタンプが0なら通常アセット、現在時刻+10秒ならHMRアセット。タイムスタンプが現在時刻以上のアセットは削除対象外。

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | CleanPlugin.js | `lib/CleanPlugin.js` | `apply()` メソッド（381-491行目）がエントリーポイント |

**主要処理フロー**:
1. **382行目**: dry, keepオプションの取得
2. **385-392行目**: keepFnの構築（文字列/正規表現/関数の分岐）
3. **400-490行目**: emitフック（stage:100）でのクリーンアップ処理

#### Step 3: 差分計算を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | CleanPlugin.js | `lib/CleanPlugin.js` | `getDiffToFs()` 関数（102-139行目）- FS走査による差分検出 |
| 3-2 | CleanPlugin.js | `lib/CleanPlugin.js` | `getDiffToOldAssets()` 関数（146-155行目）- メモリ差分計算 |
| 3-3 | CleanPlugin.js | `lib/CleanPlugin.js` | `getDirectories()` 関数（72-91行目）- ディレクトリ一覧の算出 |

#### Step 4: 差分適用を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | CleanPlugin.js | `lib/CleanPlugin.js` | `applyDiff()` 関数（182-341行目）- ファイル削除の実行 |

**主要処理フロー**:
- **243-297行目**: "check"ジョブ - stat/readdirで種別判定
- **298-315行目**: "rmdir"ジョブ - ディレクトリ削除
- **316-333行目**: "unlink"ジョブ - ファイル削除

### プログラム呼び出し階層図

```
CleanPlugin.apply(compiler)
    |
    +-- compiler.hooks.emit.tapAsync(stage: 100)
            |
            +-- 現在アセット一覧構築
            |
            +-- [初回] getDiffToFs(fs, outputPath, currentAssets)
            |       |
            |       +-- getDirectories(currentAssets)
            |       +-- fs.readdir() で各ディレクトリを走査
            |
            +-- [2回目以降] getDiffToOldAssets(currentAssets, oldAssets)
            |
            +-- applyDiff(fs, outputPath, dry, logger, diff, isKept)
                    |
                    +-- processAsyncTree(jobs, 10, ...)
                            |
                            +-- [check] doStat() → unlink or rmdir
                            +-- [unlink] fs.unlink()
                            +-- [rmdir] fs.rmdir()
```

### データフロー図

```
[入力]                          [処理]                              [出力]

compilation.assets ──────▶ 現在アセット一覧 ──┐
                                              ├──▶ diff算出 ──▶ applyDiff ──▶ ファイル削除
OutputFileSystem ────────▶ FS走査結果 ────────┘                              ディレクトリ削除
(or oldAssets)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| CleanPlugin.js | `lib/CleanPlugin.js` | ソース | 本機能のメインプラグイン（496行） |
| Compilation.js | `lib/Compilation.js` | ソース | アセット一覧の提供元 |
| processAsyncTree.js | `lib/util/processAsyncTree.js` | ソース | 非同期ツリー処理ユーティリティ |
| fs.js | `lib/util/fs.js` | ソース | ファイルシステム抽象化・join関数 |
| WebpackOptions.json | `schemas/WebpackOptions.json` | 設定 | CleanOptionsのスキーマ定義 |
