# 機能設計書 75-レジストリ（プラグインシステム）

## 概要

本ドキュメントは、Decap CMSのグローバルレジストリ（プラグインシステム）の機能設計を記述する。

### 本機能の処理概要

本機能は、バックエンド、ウィジェット、プレビューテンプレート、プレビュースタイル、メディアライブラリ、ロケール、イベントリスナー、エディタコンポーネント、Remarkプラグイン、ウィジェット値シリアライザー、カスタムフォーマットを登録・取得するグローバルレジストリを提供する。CMSの拡張性の基盤となるプラグインアーキテクチャである。

**業務上の目的・背景**：Decap CMSは多様なGitバックエンド、ウィジェット、メディアライブラリをサポートする必要がある。グローバルレジストリにより、これらの拡張機能をプラグインとして動的に登録・解決する仕組みを提供し、コアとプラグインの疎結合を実現する。サードパーティ開発者もカスタムウィジェットやバックエンドを開発・登録できる。

**機能の利用シーン**：アプリケーション起動時にデフォルトのバックエンド・ウィジェット・メディアライブラリが登録される。また、CMS.registerWidget()、CMS.registerBackend()等のAPIを通じて、外部からのプラグイン登録にも使用される。

**主要な処理内容**：
1. **バックエンド登録/取得**（registerBackend/getBackend）: Gitバックエンド実装をname + BackendClassのペアで登録し、init関数でインスタンス化
2. **ウィジェット登録/取得**（registerWidget/getWidget/getWidgets/resolveWidget）: ウィジェットのcontrol、preview、schemaを登録。配列一括登録・オブジェクト形式登録・文字列参照登録に対応
3. **プレビューテンプレート登録/取得**（registerPreviewTemplate/getPreviewTemplate）: コレクション名に対応するプレビューコンポーネントを登録
4. **プレビュースタイル登録/取得**（registerPreviewStyle/getPreviewStyles）: プレビューペインに適用するCSSスタイルを登録。rawオプションでCSS文字列も対応
5. **メディアライブラリ登録/取得**（registerMediaLibrary/getMediaLibrary）: 外部メディアライブラリ（Cloudinary、Uploadcare等）を登録
6. **ロケール登録/取得**（registerLocale/getLocale）: CMS UIのローカライズ用フレーズを登録
7. **イベントリスナー登録/取得/削除/実行**（registerEventListener/getEventListeners/removeEventListener/invokeEvent）: preSave/postSave/prePublish/postPublish/preUnpublish/postUnpublishのイベントフック管理
8. **エディタコンポーネント登録/取得**（registerEditorComponent/getEditorComponents）: Markdownエディタのカスタムブロックを登録
9. **Remarkプラグイン登録/取得**（registerRemarkPlugin/getRemarkPlugins）: Markdownパーサーのカスタムプラグインを登録
10. **ウィジェット値シリアライザー登録/取得**（registerWidgetValueSerializer/getWidgetValueSerializer）: ウィジェット固有のシリアライズ/デシリアライズ処理を登録
11. **カスタムフォーマット登録/取得**（registerCustomFormat/getCustomFormats/getCustomFormatsExtensions/getCustomFormatsFormatters）: カスタムファイルフォーマット（拡張子+フォーマッタ）を登録

**関連システム・外部連携**：immutable.js（editorComponentsのMap管理）、immer（getWidgetsのproduce）、EditorComponentバリューオブジェクト（エディタコンポーネント正規化）と連携する。

**権限による制御**：レジストリ自体には権限制御はない。グローバルスコープでのCMS.register*() APIとして公開される。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | 全画面共通 | 参照 | バックエンド、ウィジェット、ロケール等はすべての画面で暗黙的に使用される |
| 1 | ログイン画面 | 参照画面 | 登録されたバックエンドの認証ページを表示 |
| 6 | エントリ編集画面 | 参照画面 | 登録されたウィジェット、プレビューテンプレート、エディタコンポーネントを使用 |
| 9 | メディアライブラリ画面 | 参照画面 | 登録されたメディアライブラリを使用 |

## 機能種別

プラグインレジストリ / 拡張機能管理

## 入力仕様

### 入力パラメータ

**registerBackend**

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes | バックエンド名 | null/undefinedチェック、重複チェック |
| BackendClass | class | Yes | バックエンドクラス | null/undefinedチェック |

**registerWidget（文字列形式）**

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes | ウィジェット名 | - |
| control | Component \| string | Yes | コントロールコンポーネントまたは既存ウィジェット名 | - |
| preview | Component | No | プレビューコンポーネント | - |
| schema | object | No | バリデーションスキーマ（デフォルト: {}） | - |

**registerWidget（オブジェクト形式）**

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes | ウィジェット名 | - |
| controlComponent | Component | Yes | コントロールコンポーネント | 未指定時Error |
| previewComponent | Component | No | プレビューコンポーネント | - |
| schema | object | No | バリデーションスキーマ | - |
| allowMapValue | boolean | No | Map値許可フラグ | - |
| globalStyles | object | No | グローバルスタイル | - |

**registerEventListener**

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes | イベント名 | allowedEventsに含まれるか検証 |
| handler | function | Yes | イベントハンドラ | - |
| options | object | No | ハンドラオプション | - |

### 入力データソース

- アプリケーション起動時のデフォルトエクスポートパッケージ（decap-cms-default-exports）
- CMS.register*() API経由の外部プラグイン登録
- 設定ファイル（config.yml）からの間接的な参照

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| registry | object | グローバルレジストリオブジェクト |
| backend | object | { init: function } 形式のバックエンドファクトリ |
| widget | object | { control, preview, schema, globalStyles, allowMapValue, ...options } |
| eventHandlers | array | { handler, options } の配列 |
| editorComponents | ImmutableMap | エディタコンポーネントのImmutableMap |

### 出力先

グローバルレジストリオブジェクト（モジュールスコープの変数）に保存。get*()関数で取得可能。

## 処理フロー

### 処理シーケンス

```
1. レジストリ初期化
   └─ registry変数が backends:{}, templates:{}, previewStyles:[], widgets:{}, editorComponents:Map(), remarkPlugins:[], widgetValueSerializers:{}, mediaLibraries:[], locales:{}, eventHandlers:{}, formats:{} で初期化
2. プラグイン登録
   └─ 各register*()関数でregistry内の対応する領域に登録
3. プラグイン取得
   └─ 各get*()関数でregistryから該当データを取得
4. イベント実行
   └─ invokeEvent()で登録されたハンドラを順次実行。戻り値によるエントリデータの変更をサポート
```

### フローチャート

```mermaid
flowchart TD
    A[レジストリ初期化] --> B{プラグイン登録}
    B -->|バックエンド| C[registerBackend: name + init関数]
    B -->|ウィジェット| D[registerWidget: control + preview + schema]
    B -->|イベント| E[registerEventListener: name + handler]
    B -->|その他| F[registerPreviewTemplate, registerLocale等]
    C --> G[registry.backends に格納]
    D --> H[registry.widgets に格納]
    E --> I[registry.eventHandlers に格納]
    F --> J[対応する registry フィールドに格納]
    G --> K{プラグイン利用}
    H --> K
    I --> K
    J --> K
    K -->|取得| L[get* 関数で取得]
    K -->|実行| M[invokeEvent でハンドラ実行]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-75-01 | バックエンド重複禁止 | 同一名のバックエンドは二重登録できない（console.errorで警告） | registerBackend時 |
| BR-75-02 | ウィジェット上書き警告 | 同一名のウィジェットが既に存在する場合、console.warnで警告し上書きする | registerWidget(object形式)時 |
| BR-75-03 | ウィジェットcontrol必須 | オブジェクト形式でcontrolComponentが未指定の場合、Errorをスロー | registerWidget(object形式)時 |
| BR-75-04 | ウィジェット文字列参照 | controlに文字列を指定した場合、既存ウィジェットのcontrolを参照コピーする | registerWidget(string形式)時 |
| BR-75-05 | イベント名検証 | allowedEvents以外のイベント名が指定された場合、Errorをスロー | register/get/remove/invokeEvent時 |
| BR-75-06 | allowedEvents | 'prePublish', 'postPublish', 'preUnpublish', 'postUnpublish', 'preSave', 'postSave'の6種類 | 常時 |
| BR-75-07 | code-block唯一性 | エディタコンポーネントでtype='code-block'は1つのみ登録可能。既存があれば上書き | registerEditorComponent時 |
| BR-75-08 | メディアライブラリ重複禁止 | 同一名のメディアライブラリは二重登録不可（Errorをスロー） | registerMediaLibrary時 |
| BR-75-09 | resolveWidgetフォールバック | 指定名のウィジェットが見つからない場合、'string'ウィジェットを返す。それもなければ'unknown'を返す | resolveWidget時 |
| BR-75-10 | invokeEventチェーン | イベントハンドラの戻り値がundefinedでない場合、エントリデータを更新して次のハンドラに渡す | invokeEvent時 |

### 計算ロジック

特になし。

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

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

本機能はインメモリのグローバルレジストリであり、データベース操作は行わない。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | Error (throw) | registerWidget(object)でcontrolComponent未指定 | Widget "${name}" registered without `controlComponent` |
| - | Error (throw) | 無効なイベント名指定 | Invalid event name '${name}' |
| - | Error (throw) | メディアライブラリ重複登録 | A media library named ${name} has already been registered |
| - | console.error | registerBackendでname/BackendClass未指定 | パラメータ不正の警告 |
| - | console.error | registerBackendで重複name | Backend [${name}] already registered |
| - | console.error | registerWidgetで不正引数 | registerWidget failed, called with incorrect arguments |
| - | console.warn | registerWidget(object)で既存ウィジェット上書き | Multiple widgets registered with name "${name}" |
| - | console.warn | registerEditorComponentでcode-block二重登録 | Only one editor component of type "code-block" |

### リトライ仕様

リトライは不要。同期的なレジストリ操作のみ。

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

トランザクションは存在しない。レジストリ操作は即時反映。

## パフォーマンス要件

- レジストリの登録・取得はO(1)のハッシュマップアクセスであること
- getWidgets()はimmer produceで変換するため、ウィジェット数に比例した処理時間

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

- レジストリはグローバルスコープでアクセス可能であり、サードパーティプラグインが任意のバックエンド・ウィジェットを登録できる
- registerBackendのinitファクトリ関数はコンストラクタ呼び出しを行うため、信頼できないコードの登録には注意が必要
- イベントハンドラはエントリデータを変更できるため、preSave等でデータ改竄のリスクがある

## 備考

- registryはモジュールスコープのクロージャ変数であり、シングルトンパターンで実装されている
- editorComponentsはImmutable.jsのMapで管理されており、他のレジストリ領域（プレーンオブジェクト/配列）とは異なるデータ構造
- default exportとしてregister*/get*関数群をまとめたオブジェクトがエクスポートされている

---

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

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

### 推奨読解順序

#### Step 1: レジストリの全体構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | registry.js | `packages/decap-cms-core/src/lib/registry.js` | registryオブジェクトのフィールド構成とエクスポート一覧 |

**読解のコツ**: registry.jsは単一ファイルに全プラグイン種別の登録/取得ロジックが集約されている。まずregistryオブジェクトの構造（23-35行目）を把握し、次にdefault export（37-66行目）で公開APIの一覧を確認するとよい。

**主要処理フロー**:
- **7-14行目**: allowedEvents定義と初期化（preSave/postSave/prePublish/postPublish/preUnpublish/postUnpublish）
- **23-35行目**: registryオブジェクト初期化（backends, templates, previewStyles, widgets, editorComponents, remarkPlugins, widgetValueSerializers, mediaLibraries, locales, eventHandlers, formats）
- **37-66行目**: default export - 全register/get関数のエクスポート

#### Step 2: ウィジェット登録ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | registry.js | `packages/decap-cms-core/src/lib/registry.js` | registerWidgetの3つの登録パターン（配列、文字列、オブジェクト） |

**主要処理フロー**:
- **94-138行目**: registerWidget
  - **95-102行目**: 配列形式 - 各要素を再帰的にregisterWidget呼び出し
  - **103-107行目**: 文字列形式 - controlが文字列の場合は既存ウィジェットのcontrolを参照
  - **108-137行目**: オブジェクト形式 - widgetName, controlComponent, previewComponent等を分割代入
- **139-141行目**: getWidget - 名前でウィジェット取得
- **142-146行目**: getWidgets - immer produceで全ウィジェットを配列変換
- **147-149行目**: resolveWidget - フォールバック付きウィジェット解決

#### Step 3: バックエンド登録ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | registry.js | `packages/decap-cms-core/src/lib/registry.js` | registerBackend/getBackendのファクトリパターン |

**主要処理フロー**:
- **200-212行目**: registerBackend - name/BackendClassの検証後、init関数（new BackendClass(...)）を登録
- **214-216行目**: getBackend - 名前でバックエンド取得

#### Step 4: イベントリスナーロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | registry.js | `packages/decap-cms-core/src/lib/registry.js` | registerEventListener/invokeEventのチェーン実行ロジック |

**主要処理フロー**:
- **232-236行目**: validateEventName - allowedEventsに含まれるか検証
- **238-241行目**: getEventListeners - スプレッド演算子でコピーを返す
- **243-246行目**: registerEventListener - { handler, options } を配列に追加
- **248-261行目**: invokeEvent - 登録ハンドラを順次実行。戻り値でエントリデータを更新
- **263-272行目**: removeEventListener - 特定ハンドラまたは全ハンドラの削除

#### Step 5: エディタコンポーネント登録を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | registry.js | `packages/decap-cms-core/src/lib/registry.js` | registerEditorComponentのcode-block唯一性制約 |
| 5-2 | EditorComponent.js | `packages/decap-cms-core/src/valueObjects/EditorComponent.js` | createEditorComponentによるコンポーネント正規化 |

**主要処理フロー**:
- **154-169行目**: registerEditorComponent - EditorComponent()で正規化後、ImmutableMapに登録
  - **156-164行目**: code-blockタイプの唯一性チェックと上書き処理
- **EditorComponent.js 10-38行目**: createEditorComponent - id/label/type/icon/pattern/fields/fromBlock/toBlock/toPreview等のデフォルト値設定

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

```
CMS API / デフォルトエクスポート
    |
    +-- registerBackend(name, BackendClass)
    |       +-- registry.backends[name] = { init: (...args) => new BackendClass(...args) }
    |
    +-- registerWidget(name/array/object, control, preview, schema)
    |       +-- registry.widgets[name] = { control, preview, schema, ... }
    |
    +-- registerPreviewTemplate(name, component)
    |       +-- registry.templates[name] = component
    |
    +-- registerPreviewStyle(style, opts)
    |       +-- registry.previewStyles.push({ ...opts, value: style })
    |
    +-- registerEditorComponent(component)
    |       +-- EditorComponent(component) [正規化]
    |       +-- registry.editorComponents.set(plugin.id, plugin)
    |
    +-- registerRemarkPlugin(plugin)
    |       +-- registry.remarkPlugins.push(plugin)
    |
    +-- registerWidgetValueSerializer(widgetName, serializer)
    |       +-- registry.widgetValueSerializers[widgetName] = serializer
    |
    +-- registerMediaLibrary(mediaLibrary, options)
    |       +-- registry.mediaLibraries.push({ ...mediaLibrary, options })
    |
    +-- registerLocale(locale, phrases)
    |       +-- registry.locales[locale] = phrases
    |
    +-- registerEventListener({ name, handler }, options)
    |       +-- registry.eventHandlers[name].push({ handler, options })
    |
    +-- registerCustomFormat(name, extension, formatter)
            +-- registry.formats[name] = { extension, formatter }
```

### データフロー図

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

プラグイン登録              -->  register*() 関数群          -->  registry オブジェクト
  (name, component,              (バリデーション + 格納)          (インメモリ)
   BackendClass, handler等)             |
                                        v
プラグイン取得要求          -->  get*() 関数群               -->  登録済みプラグイン
  (name指定)                     (ハッシュマップ検索)             (component, handler等)
                                        |
イベント発火                -->  invokeEvent()               -->  変更済みエントリデータ
  ({ name, data })               (ハンドラ順次実行)               (data.entry.get('data'))
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| registry.js | `packages/decap-cms-core/src/lib/registry.js` | ソース | グローバルレジストリ本体 |
| EditorComponent.js | `packages/decap-cms-core/src/valueObjects/EditorComponent.js` | ソース | エディタコンポーネント正規化 |
| bootstrap.js | `packages/decap-cms-core/src/bootstrap.js` | ソース | レジストリの初期利用（起動時のバックエンド解決等） |
