Color mode

プラグイン作成の詳細

このページは、Riebeckite Plugin を実際に設計・実装するときの詳細ガイドです。

初めて Plugin を作る場合は、先に はじめてのプラグイン作成 を読んでください。

このページでは、その先に必要になる、

  • どの拡張ポイントを使うか
  • Plugin 同士の依存関係
  • Lifecycle
  • Content Pipeline
  • Renderer / Page Type
  • CSS / Browser 処理
  • Cache / Diagnostics
  • Package としての配布

までをまとめて扱います。

各型やフィールドの完全な定義を確認したい場合は Plugin API を参照してください。

1. Plugin にするべき機能

Plugin は Riebeckite に再利用可能な機能を追加する仕組みです。

たとえば、

  • Markdown / HTML の解釈
  • Content Transformation
  • 独自形式の Renderer
  • Browser Behavior
  • Diagnostics
  • HTTP Endpoint
  • SEO
  • 独立ページ

などを実装できます。

一方、すべての拡張を Plugin にするわけではありません。

Diagram source
text
flowchart TD
    Q{"何を追加する?"}
 
    Q -->|"Framework共通のContent Model"| Core["Core"]
    Q -->|"再利用可能な機能"| Plugin["Plugin"]
    Q -->|"HonoX / Viteとの接続"| Integration["Integration"]
    Q -->|"Site固有Route / Layout"| App["Application"]
    Q -->|"見た目だけ"| Theme["Theme"]

特に、

text
見た目だけ
  → Theme
 
Site 固有の Route / Layout
  → Application
 
HonoX / Vite との接続
  → Integration
 
Framework 全体の Content Model
  → Core

です。

「何でも Plugin にする」のではなく、その機能がどの責務に属するかを先に判断してください。

2. 最小の Plugin

Plugin は definePlugin() で定義します。

ts
import { definePlugin } from "@riebeckite/core";
 
export function examplePlugin() {
  return definePlugin({
    name: "example",
  });
}

Site では riebeckite.config.ts の plugins に追加します。

ts
export default defineConfig({
  plugins: [
    examplePlugin(),
  ],
});

これが最小構成です。

3. Options を追加する

Plugin に設定が必要なら Factory の引数として受け取ります。

ts
type ExampleOptions = {
  enabled?: boolean;
};
 
export function examplePlugin(
  options: ExampleOptions = {},
) {
  return definePlugin({
    name: "example",
    options,
  });
}

利用側では、

ts
export default defineConfig({
  plugins: [
    examplePlugin({
      enabled: true,
    }),
  ],
});

のように指定できます。

条件付き Plugin も利用できます。

ts
plugins: [
  condition && myPlugin(),
]

false、null、undefined は Plugin Input の解決時に除外されます。

また、

ts
enabled: false

の Plugin も実行されません。

4. 拡張ポイントを選ぶ

Plugin を実装するときは、必要な最小の拡張ポイントを選びます。

Diagram source
text
flowchart TD
    Q{"何を実装する?"}
 
    Q -->|"Markdown / HTML変換"| Pipeline["remark / rehype"]
    Q -->|"Content処理の特定段階"| Hook["Content Hooks"]
    Q -->|"記事内の特殊表示"| Renderer["renderers"]
    Q -->|"独立ページ"| Page["pageTypes"]
    Q -->|"Graph拡張"| Graph["extendContentGraph"]
    Q -->|"URL規則"| Location["resolveContentLocations"]
    Q -->|"CSS"| Asset["assets"]
    Q -->|"Browser処理"| Client["clientEntries"]
    Q -->|"HTTP"| Endpoint["endpoints"]
    Q -->|"SEO"| SEO["seo"]
    Q -->|"診断"| Diagnostics["addDiagnostics"]

現在の主な Contract は次のとおりです。

領域 API
Identity name, enabled, order, options
Dependency provides, requires, optional
Validation validateOptions
Cache cacheVersion, context.cache
Lifecycle setup, buildStart, buildEnd, dispose
Content Hooks onConfigResolved, onContentLoaded, onPostParsed, onPostProcessed, onManifestCreated
Public Location resolveContentLocations
Pipeline remarkPlugins, rehypePlugins, extendMarkdownPipeline, extendHtmlPipeline
Graph extendContentGraph
Diagnostics addDiagnostics
本文内の描画 renderers
独立ページ pageTypes
Browser assets, clientEntries
HTTP endpoints
SEO seo

すべてを実装する必要はありません。

UI や output の拡張ポイントは複数あり、優劣の順列ではなく選択肢です。Markdown / HTML 変換、renderer、Page Type、body Slot、公開する Hono JSX component、client entry があります。どれを選ぶかは UI の提供方法 を参照してください。

5. Plugin の依存関係

Plugin 同士に実際の依存関係がある場合は Capability Contract を使用します。

ts
definePlugin({
  name: "consumer",
 
  provides: [
    "example.output",
  ],
 
  requires: [
    "content.graph",
  ],
 
  optional: [
    "example.optional",
  ],
});

それぞれ、

Field 意味
provides この Plugin が提供する Capability
requires 必須の Capability
optional あれば利用する Capability

です。

Resolver は依存関係をもとに実行順を解決します。

Diagram source
text
flowchart LR
    Provider["Provider<br/>provides: content.graph"]
    Consumer["Consumer<br/>requires: content.graph"]
 
    Provider --> Consumer

次の状態は Configuration Error になります。

  • 必須 Capability がない
  • Provider が重複している
  • Dependency Cycle がある

order は依存解決前の基本順序です。

実際の依存関係を表現するために order を使わないでください。

6. Options を検証する

TypeScript の型だけでは Runtime Value を完全には保証できません。

必要な Plugin は validateOptions を実装します。

text
Options
   ↓
validateOptions
   ↓
Structured Issues
   ↓
riebeckite check

Validator は副作用を持たせません。

特に、

  • Filesystem Scan
  • Build
  • Cache Write
  • 外部状態の変更

を行わないでください。

問題は Structured Issue として返し、Config Validation からまとめて表示できるようにします。

7. Plugin Context

Plugin は Framework の Service を PluginContext から受け取ります。

基本的な Context は概念的に次のようになります。

ts
type PluginContext = {
  config?: ResolvedRiebeckiteConfig;
  contentIndex: Map<string, string>;
  diagnostics: Diagnostic[];
  cache: PluginCache;
  output: GeneratedOutputSink;
  logger: Logger;
  tracer: Tracer;
  contentSource?: ContentSource;
};

Hook によって、

text
slug
markdown
content
manifest
entries
location input

などが追加されます。

Plugin 内で Framework Service の Global Singleton を作るのではなく、Context から必要な Service を受け取ることを優先します。

8. Lifecycle

Framework Lifecycle には、

text
setup
buildStart
buildEnd
dispose

があります。

概念的には、

Diagram source
text
flowchart LR
    Setup["setup"]
    Start["buildStart"]
    Work["Build / Content Processing"]
    End["buildEnd"]
    Dispose["dispose"]
 
    Setup --> Start
    Start --> Work
    Work --> End
    End --> Dispose

という流れになります。

setup、buildStart、onConfigResolved、Content 処理、buildEnd は、1つの ContentManager につき一度だけ実行されます。buildEnd は Diagnostics の収集後に完成した Manifest を受け取る唯一の終端 Hook です。dispose は確保した Resource の解放に使用し、解決済み Plugin の逆順で実行されます。

Lifecycle の実行順は、解決済み Plugin Order に従います。

Hook で Error が発生した場合は、

  • Plugin 名
  • Hook 名
  • 元の Cause

が分かる形で上位へ伝播させてください。

9. Content Lifecycle

Content は複数の段階を通って処理されます。

Diagram source
text
flowchart TD
    Config["Config Resolved"]
    Loaded["Content Loaded"]
    Location["Public Location Resolved"]
    Parsed["Post Parsed"]
    Processed["Post Processed"]
    Graph["Content Graph"]
    Manifest["Manifest Created"]
 
    Config --> Location
    Location --> Loaded
    Loaded --> Parsed
    Parsed --> Processed
    Processed --> Graph
    Graph --> Manifest

全体の順序は setup → buildStart → onConfigResolved → Public Location 解決 → onContentLoaded → Markdown / HTML Pipeline → onPostParsed → onPostProcessed → extendContentGraph → onManifestCreated → Diagnostics → buildEnd です。

代表的な Hook は、

text
onConfigResolved
onContentLoaded
onPostParsed
onPostProcessed
onManifestCreated

です。

必要な段階の Hook だけを使用してください。

たとえば Manifest にすでに存在する情報を onContentLoaded で独自に再構築する、といった実装は避けます。

10. Markdown / HTML Pipeline

Markdown や HTML の意味を変換する場合は remark / rehype を利用します。

単純な Plugin なら、

ts
definePlugin({
  name: "example",
 
  remarkPlugins: [
    remarkExample,
  ],
 
  rehypePlugins: [
    rehypeExample,
  ],
});

と宣言できます。

Pipeline 自体を構成する必要がある場合は、

ts
definePlugin({
  name: "example",
 
  extendMarkdownPipeline(pipeline) {
    pipeline.use(remarkExample);
  },
 
  extendHtmlPipeline(pipeline) {
    pipeline.use(rehypeExample);
  },
});

を使用します。

Markdown / HTML の意味変換を Application Component に持ち込まず、Plugin の Pipeline 処理として実装するのが基本です。

11. Content Graph

Content Graph を拡張する場合は extendContentGraph を使用します。

Diagram source
text
flowchart LR
    Manifest["Manifest"]
    Graph["Content Graph"]
    Plugin["extendContentGraph"]
    Result["Extended Graph"]
 
    Manifest --> Graph
    Graph --> Plugin
    Plugin --> Result

Backlinks や Graph 系機能を実装するために、Plugin が Filesystem を再走査しないでください。

すでに解決された Manifest / Content Graph を利用します。

12. Public Location

Content の公開 URL を変更する Plugin では resolveContentLocations を使用します。

最初に Core が Default Location を解決します。

text
index
  → /
 
その他
  → /{slug}

その後、解決済み Plugin Order に従って Location Hook が実行されます。

Diagram source
text
flowchart LR
    Content["Content"]
    Default["Default Resolver"]
    PluginA["Plugin A"]
    PluginB["Plugin B"]
    Location["ContentPublicLocation"]
 
    Content --> Default
    Default --> PluginA
    PluginA --> PluginB
    PluginB --> Location

結果は ContentPublicLocation として、

  • Manifest
  • Content Graph
  • Markdown Pipeline

などから利用されます。

URL Strategy は Plugin が所有します。

Core は特定 Plugin の URL 規則を知りません。

Consumer は最終的な、

ts
entry.permalink

を利用します。

Location を解決できない場合は、slug へ暗黙的に fallback せず明示的な Error とします。

13. Renderers

renderers は記事本文内の特殊な Content Target を HTML へ変換します。

たとえば、

text
Canvas
Bases
Excalidraw
Attachment
Media

などです。

Renderer Context には、

text
kind
path
raw
label
url
embed

と通常の PluginContext が含まれます。

Diagram source
text
flowchart TD
    Target["Content Target"]
    Renderer{"このRendererが処理する?"}
 
    Target --> Renderer
    Renderer -->|Yes| HTML["HTML"]
    Renderer -->|No| Next["次のRenderer"]

処理対象でなければ null を返します。

これによって複数 Renderer が同じ Pipeline に参加できます。

14. Page Types

独立した URL を持つ画面を Plugin が提供する場合は pageTypes を使用します。

たとえば、

text
/explore
/report
/tags/example

などです。

Page Type は、

  • 一意な id
  • SSG 用 paths
  • 必要に応じた priority
  • resolve

を持ちます。

Resolver は Framework 非依存の HTML Body または null を返します。

必要なら、

text
title
description
headTags

も返せます。

ただし Document Frame は Site Application が所有します。

Diagram source
text
flowchart LR
    Plugin["Plugin Page Type"]
    Core["Core Resolver"]
    Integration["HonoX Integration"]
    Site["Site Document Frame"]
 
    Plugin --> Core
    Core --> Integration
    Integration --> Site

Taxonomy の一覧や Explorer のような独立画面には Page Type を使用します。

Canvas、Bases、Excalidraw のような記事本文への埋め込みには Renderer を使用します。

Plugin が HonoX Route File や Document Frame を所有しないことが重要です。

Page Type の詳しい仕組みは Page System を参照してください。

15. Assets

Plugin 固有の Stylesheet は Plugin Package 内に置き、assets から公開します。

ts
assets: [
  {
    pluginName: "example",
    kind: "style",
    moduleSpecifier:
      "@riebeckite/plugin-example/style.css",
  },
]

Integration がこの Module Specifier を解決して Browser へ届けます。

Diagram source
text
flowchart LR
    CSS["Plugin style.css"]
    Asset["assets"]
    Integration["Integration"]
    Browser["Browser"]
 
    CSS --> Asset
    Asset --> Integration
    Integration --> Browser

Plugin 固有 CSS を apps/web にコピーしたり、Browser から /node_modules を直接参照させたりしないでください。

16. CSS Hooks

再利用可能な UI を Plugin が描画する場合は、最外要素に Stable Root Hook を付けます。

Plugin / Feature Hook は、

text
rr-<feature>

とします。

たとえば、

text
rr-search
rr-callout
rr-query
rr-code

です。

内部要素では BEM を利用できます。

text
rr-search
rr-search__input
rr-search__result
rr-search--loading

Namespace の役割は次のとおりです。

Namespace 用途
rb-* Framework の構造 Hook
--rb-* Framework の Semantic Token
rr-* Plugin / Feature Hook
--rr-* Plugin 固有 Token

Plugin の出力を rb-* Namespace に置かないでください。

既存 Class がある場合は削除せず、rr-* Hook を追加します。

また、

text
rr-feature__*
rr-feature--*

は原則として内部実装です。

Theme から利用してよい子孫 Class だけを Public Hook として文書化してください。

17. Client Entries

Browser 上で初期化処理が必要な場合だけ clientEntries を使用します。

ts
clientEntries: [
  {
    pluginName: "example",
    moduleSpecifier:
      "@riebeckite/plugin-example/client",
    exportName: "initExample",
 
    publicConfig: {
      selector: ".example",
    },
  },
]
Diagram source
text
flowchart LR
    Plugin["Plugin"]
    Client["Client Entry"]
    Build["Integration"]
    Browser["Browser"]
 
    Plugin --> Client
    Client --> Build
    Build --> Browser

SSR / Build-time だけで完結する Plugin に Client JavaScript を追加しないでください。

publicConfig は Browser へ渡されるため、

  • Token
  • Credential
  • Secret
  • Private Service URL

などを含めてはいけません。

Plugin の options が自動的に Client へ渡されることもありません。

18. Endpoints / SEO

HTTP Endpoint を提供する場合は endpoints Contract を使用します。

Diagram source
text
flowchart LR
    Plugin["Plugin"]
    Endpoint["Endpoint Contract"]
    Integration["Integration"]
    Router["Host Router"]
 
    Plugin --> Endpoint
    Endpoint --> Integration
    Integration --> Router

HonoX など特定の Route Framework を Plugin 本体へ直接組み込まないでください。

SEO へ参加する場合は seo を使用します。

たとえば Metadata や Feed に Plugin が情報を追加できます。

Application Route 側へ Plugin 固有 SEO Logic を再実装しないことが重要です。

19. Diagnostics

Plugin 固有の問題は addDiagnostics から報告します。

ts
addDiagnostics(context) {
  return [
    {
      // Diagnostic contract
    },
  ];
}

診断結果は可能な限り Structured Data として返してください。

Plugin が、

ts
console.log(...)

で独自の CLI Output を作るのではなく、Diagnostics または Logger を使用します。

20. Plugin Cache

context.cache は Plugin ごとに分離された Build-time Cache です。

保存するデータには次の条件があります。

  • 再生成できる
  • JSON Serializable
  • Plugin Namespace 内で完結する
  • 壊れていても安全に Cache Miss として扱える

cacheVersion を使って Cache Format の互換性を管理できます。

Write は Atomic に行います。

Diagram source
text
flowchart TD
    Work["Plugin Work"]
    Cache{"有効なCache?"}
 
    Work --> Cache
 
    Cache -->|Yes| Reuse["Reuse"]
    Cache -->|No| Generate["Regenerate"]

これは Runtime Database ではありません。

Cloudflare Workers などの永続 Storage として使用しないでください。

21. Logger / Tracer

Plugin では Framework の Logger / Tracer を利用できます。

ts
context.logger.info("...");
 
await context.tracer.span(
  "plugin.example.work",
  {
    plugin: "example",
  },
  async () => {
    // work
  },
);

Logger は処理内容を記録し、Tracer は処理時間などを Structured Trace として記録します。

Profiler はこの Trace を利用するため、Plugin ごとに独自の Stopwatch や Profiling System を作る必要はありません。

22. Site 内だけで使う Plugin

Plugin は npm に公開しなくても利用できます。

Site 内に、

text
site/
└─ extensions/
   └─ local-plugin.ts

のように置いて definePlugin() できます。

ts
// site/extensions/local-plugin.ts
 
return definePlugin({
  name: "site-local",
 
  assets: [
    {
      pluginName: "site-local",
      kind: "style",
      moduleSpecifier:
        "/extensions/plugin.css",
    },
  ],
});

Site-local Plugin も Published Plugin と同じ、

  • Dependency Resolution
  • Pipeline
  • Hooks
  • Diagnostics
  • Renderer
  • Page Type

などの Contract を利用します。

createStyleAsset() と createClientEntry() は @riebeckite/plugin-<name>/... の specifier しか組み立てないため、その名前で ない package(未公開 Plugin、別名の公開 package)は Host Bundler が解決できる moduleSpecifier を直接指定してください。

23. Package として配布する

Plugin を再利用可能な Package として配布する場合は、たとえば次の構成にできます。

text
packages/plugins/example/
├─ index.ts
├─ components/       # 必要な場合のみ
├─ client.ts         # 必要な場合のみ
├─ src/
│  ├─ remark.ts
│  ├─ rehype.ts
│  ├─ renderer.ts
│  └─ types.ts
├─ style.css         # 必要な場合のみ
├─ package.json
├─ README_ja.md
└─ README.md

Riebeckite repository 内では packages/plugins/backlinks が参考になります。

公開 package では Build 済み ESM と型定義を publish し、exports をその成果物へ 向け、prepack script で build します。Repository の build script は publish されないため、esbuild(format: "esm"、packages: "external"、 external: ["@riebeckite/*"])と tsc --emitDeclarationOnly による小さな build を 用意してください。最小構成の package.json は Repository 外で Plugin を配布する を参照してください。

ただし、すべての Plugin に client.ts、style.css、components/ が必要なわけではありません。

必要なものだけを作成してください。

24. Repository 外で配布する

外部 Plugin Package は Riebeckite monorepo の内部構造に依存させません。

基本的には、

text
@riebeckite/core

の Public API を利用します。

Plugin 自身が持つ、

text
./client
./components
./style.css

などは、自身の package.json の exports で公開します。

次のような Internal Import は使用しません。

ts
import {
  something,
} from "@riebeckite/core/src/...";

monorepo 内にしか存在しない相対 Path にも依存しないでください。

25. ESM

NodeNext / ESM Package では、Build 後の JavaScript を Node.js が実際に解決できる必要があります。

Development 時だけ TypeScript Loader が、

text
extensionless import

などを解決できている状態に依存しないでください。

Package の正しさは Source Code だけではなく、Build 後の配布形式でも確認する必要があります。

26. Plugin を検証する

実装後は、小さい範囲から順番に確認します。

Diagram source
text
flowchart LR
    Check["check"]
    Doctor["doctor"]
    Inspect["inspect plugins"]
    Build["build"]
 
    Check --> Doctor
    Doctor --> Inspect
    Inspect --> Build

まず Configuration と Plugin Resolution を確認します。

sh
pnpm exec riebeckite check

次に Project の Health を確認します。

sh
pnpm exec riebeckite doctor

解決された Plugin を確認します。

sh
pnpm exec riebeckite inspect plugins

最後に実際の生成物まで確認します。

sh
pnpm exec riebeckite build

Plugin が解決されない場合は、まず check の出力から、

  • Import Error
  • Missing Capability
  • Duplicate Provider
  • Dependency Cycle
  • Invalid Options

などを確認してください。

27. 実装前の確認

Plugin を作り始める前に、最後に次の順番で考えると責務を分離しやすくなります。

Diagram source
text
flowchart TD
    Start["追加したい機能"]
    Plugin{"再利用可能な機能?"}
 
    Start --> Plugin
 
    Plugin -->|No| Other{"何を変える?"}
    Plugin -->|Yes| Point{"最小のExtension Pointは?"}
 
    Other -->|"見た目"| Theme["Theme"]
    Other -->|"Site固有"| App["Application"]
    Other -->|"Framework共通Model"| Core["Core"]
    Other -->|"HonoX / Vite接続"| Integration["Integration"]
 
    Point --> Implement["Pluginとして実装"]

Plugin にすると決めた後も、

その機能に本当に必要な Extension Point だけを使用する

ことが重要です。

Filesystem を再走査する前に Manifest や Content Graph が使えないか、独自 Route を追加する前に Page Type が使えないか、Client JavaScript を追加する前に SSR / Build-time だけで完結できないかを確認してください。

これによって Plugin を小さく保ち、Core、Integration、Application との不要な結合を避けられます。

関連資料

History

1 changesCollapseExpand
1 + # プラグイン作成の詳細
2 +
3 + このページは、Riebeckite Plugin を実際に設計・実装するときの詳細ガイドです。
4 +
5 + 初めて Plugin を作る場合は、先に [はじめてのプラグイン作成](../plugins/writing-a-plugin.md) を読んでください。
6 +
7 + このページでは、その先に必要になる、
8 +
9 + - どの拡張ポイントを使うか
10 + - Plugin 同士の依存関係
11 + - Lifecycle
12 + - Content Pipeline
13 + - Renderer / Page Type
14 + - CSS / Browser 処理
15 + - Cache / Diagnostics
16 + - Package としての配布
17 +
18 + までをまとめて扱います。
19 +
20 + 各型やフィールドの完全な定義を確認したい場合は [Plugin API](../reference/plugin-api.md) を参照してください。
21 +
22 + # 1. Plugin にするべき機能
23 +
24 + Plugin は **Riebeckite に再利用可能な機能を追加する仕組み**です。
25 +
26 + たとえば、
27 +
28 + - Markdown / HTML の解釈
29 + - Content Transformation
30 + - 独自形式の Renderer
31 + - Browser Behavior
32 + - Diagnostics
33 + - HTTP Endpoint
34 + - SEO
35 + - 独立ページ
36 +
37 + などを実装できます。
38 +
39 + 一方、すべての拡張を Plugin にするわけではありません。
40 +
41 + ```mermaid id="5h0kzp"
42 + flowchart TD
43 + Q{"何を追加する?"}
44 +
45 + Q -->|"Framework共通のContent Model"| Core["Core"]
46 + Q -->|"再利用可能な機能"| Plugin["Plugin"]
47 + Q -->|"HonoX / Viteとの接続"| Integration["Integration"]
48 + Q -->|"Site固有Route / Layout"| App["Application"]
49 + Q -->|"見た目だけ"| Theme["Theme"]
50 + ```
51 +
52 + 特に、
53 +
54 + ```text id="6pgq3j"
55 + 見た目だけ
56 + → Theme
57 +
58 + Site 固有の Route / Layout
59 + → Application
60 +
61 + HonoX / Vite との接続
62 + → Integration
63 +
64 + Framework 全体の Content Model
65 + → Core
66 + ```
67 +
68 + です。
69 +
70 + 「何でも Plugin にする」のではなく、その機能がどの責務に属するかを先に判断してください。
71 +
72 + # 2. 最小の Plugin
73 +
74 + Plugin は `definePlugin()` で定義します。
75 +
76 + ```ts id="sf8rf2"
77 + import { definePlugin } from "@riebeckite/core";
78 +
79 + export function examplePlugin() {
80 + return definePlugin({
81 + name: "example",
82 + });
83 + }
84 + ```
85 +
86 + Site では `riebeckite.config.ts` の `plugins` に追加します。
87 +
88 + ```ts id="32z2wh"
89 + export default defineConfig({
90 + plugins: [
91 + examplePlugin(),
92 + ],
93 + });
94 + ```
95 +
96 + これが最小構成です。
97 +
98 + # 3. Options を追加する
99 +
100 + Plugin に設定が必要なら Factory の引数として受け取ります。
101 +
102 + ```ts id="m8h0dc"
103 + type ExampleOptions = {
104 + enabled?: boolean;
105 + };
106 +
107 + export function examplePlugin(
108 + options: ExampleOptions = {},
109 + ) {
110 + return definePlugin({
111 + name: "example",
112 + options,
113 + });
114 + }
115 + ```
116 +
117 + 利用側では、
118 +
119 + ```ts id="xw3gy8"
120 + export default defineConfig({
121 + plugins: [
122 + examplePlugin({
123 + enabled: true,
124 + }),
125 + ],
126 + });
127 + ```
128 +
129 + のように指定できます。
130 +
131 + 条件付き Plugin も利用できます。
132 +
133 + ```ts id="d7esfe"
134 + plugins: [
135 + condition && myPlugin(),
136 + ]
137 + ```
138 +
139 + `false`、`null`、`undefined` は Plugin Input の解決時に除外されます。
140 +
141 + また、
142 +
143 + ```ts id="3ud7ge"
144 + enabled: false
145 + ```
146 +
147 + の Plugin も実行されません。
148 +
149 + # 4. 拡張ポイントを選ぶ
150 +
151 + Plugin を実装するときは、必要な最小の拡張ポイントを選びます。
152 +
153 + ```mermaid id="z0g35w"
154 + flowchart TD
155 + Q{"何を実装する?"}
156 +
157 + Q -->|"Markdown / HTML変換"| Pipeline["remark / rehype"]
158 + Q -->|"Content処理の特定段階"| Hook["Content Hooks"]
159 + Q -->|"記事内の特殊表示"| Renderer["renderers"]
160 + Q -->|"独立ページ"| Page["pageTypes"]
161 + Q -->|"Graph拡張"| Graph["extendContentGraph"]
162 + Q -->|"URL規則"| Location["resolveContentLocations"]
163 + Q -->|"CSS"| Asset["assets"]
164 + Q -->|"Browser処理"| Client["clientEntries"]
165 + Q -->|"HTTP"| Endpoint["endpoints"]
166 + Q -->|"SEO"| SEO["seo"]
167 + Q -->|"診断"| Diagnostics["addDiagnostics"]
168 + ```
169 +
170 + 現在の主な Contract は次のとおりです。
171 +
172 + | 領域 | API |
173 + | --- | --- |
174 + | Identity | `name`, `enabled`, `order`, `options` |
175 + | Dependency | `provides`, `requires`, `optional` |
176 + | Validation | `validateOptions` |
177 + | Cache | `cacheVersion`, `context.cache` |
178 + | Lifecycle | `setup`, `buildStart`, `buildEnd`, `dispose` |
179 + | Content Hooks | `onConfigResolved`, `onContentLoaded`, `onPostParsed`, `onPostProcessed`, `onManifestCreated` |
180 + | Public Location | `resolveContentLocations` |
181 + | Pipeline | `remarkPlugins`, `rehypePlugins`, `extendMarkdownPipeline`, `extendHtmlPipeline` |
182 + | Graph | `extendContentGraph` |
183 + | Diagnostics | `addDiagnostics` |
184 + | 本文内の描画 | `renderers` |
185 + | 独立ページ | `pageTypes` |
186 + | Browser | `assets`, `clientEntries` |
187 + | HTTP | `endpoints` |
188 + | SEO | `seo` |
189 +
190 + すべてを実装する必要はありません。
191 +
192 + UI や output の拡張ポイントは複数あり、優劣の順列ではなく選択肢です。Markdown / HTML 変換、renderer、Page Type、body Slot、公開する Hono JSX component、client entry があります。どれを選ぶかは [UI の提供方法](../plugins/writing-a-plugin.md#ui-の提供方法) を参照してください。
193 +
194 + # 5. Plugin の依存関係
195 +
196 + Plugin 同士に実際の依存関係がある場合は Capability Contract を使用します。
197 +
198 + ```ts id="mdj2qn"
199 + definePlugin({
200 + name: "consumer",
201 +
202 + provides: [
203 + "example.output",
204 + ],
205 +
206 + requires: [
207 + "content.graph",
208 + ],
209 +
210 + optional: [
211 + "example.optional",
212 + ],
213 + });
214 + ```
215 +
216 + それぞれ、
217 +
218 + | Field | 意味 |
219 + | --- | --- |
220 + | `provides` | この Plugin が提供する Capability |
221 + | `requires` | 必須の Capability |
222 + | `optional` | あれば利用する Capability |
223 +
224 + です。
225 +
226 + Resolver は依存関係をもとに実行順を解決します。
227 +
228 + ```mermaid id="ahm3ow"
229 + flowchart LR
230 + Provider["Provider<br/>provides: content.graph"]
231 + Consumer["Consumer<br/>requires: content.graph"]
232 +
233 + Provider --> Consumer
234 + ```
235 +
236 + 次の状態は Configuration Error になります。
237 +
238 + - 必須 Capability がない
239 + - Provider が重複している
240 + - Dependency Cycle がある
241 +
242 + `order` は依存解決前の基本順序です。
243 +
244 + 実際の依存関係を表現するために `order` を使わないでください。
245 +
246 + # 6. Options を検証する
247 +
248 + TypeScript の型だけでは Runtime Value を完全には保証できません。
249 +
250 + 必要な Plugin は `validateOptions` を実装します。
251 +
252 + ```text id="09xj48"
253 + Options
254 + ↓
255 + validateOptions
256 + ↓
257 + Structured Issues
258 + ↓
259 + riebeckite check
260 + ```
261 +
262 + Validator は副作用を持たせません。
263 +
264 + 特に、
265 +
266 + - Filesystem Scan
267 + - Build
268 + - Cache Write
269 + - 外部状態の変更
270 +
271 + を行わないでください。
272 +
273 + 問題は Structured Issue として返し、Config Validation からまとめて表示できるようにします。
274 +
275 + # 7. Plugin Context
276 +
277 + Plugin は Framework の Service を `PluginContext` から受け取ります。
278 +
279 + 基本的な Context は概念的に次のようになります。
280 +
281 + ```ts id="jrp2vo"
282 + type PluginContext = {
283 + config?: ResolvedRiebeckiteConfig;
284 + contentIndex: Map<string, string>;
285 + diagnostics: Diagnostic[];
286 + cache: PluginCache;
287 + output: GeneratedOutputSink;
288 + logger: Logger;
289 + tracer: Tracer;
290 + contentSource?: ContentSource;
291 + };
292 + ```
293 +
294 + Hook によって、
295 +
296 + ```text id="2t18qa"
297 + slug
298 + markdown
299 + content
300 + manifest
301 + entries
302 + location input
303 + ```
304 +
305 + などが追加されます。
306 +
307 + Plugin 内で Framework Service の Global Singleton を作るのではなく、Context から必要な Service を受け取ることを優先します。
308 +
309 + # 8. Lifecycle
310 +
311 + Framework Lifecycle には、
312 +
313 + ```text id="oy9iqs"
314 + setup
315 + buildStart
316 + buildEnd
317 + dispose
318 + ```
319 +
320 + があります。
321 +
322 + 概念的には、
323 +
324 + ```mermaid id="6hsbmh"
325 + flowchart LR
326 + Setup["setup"]
327 + Start["buildStart"]
328 + Work["Build / Content Processing"]
329 + End["buildEnd"]
330 + Dispose["dispose"]
331 +
332 + Setup --> Start
333 + Start --> Work
334 + Work --> End
335 + End --> Dispose
336 + ```
337 +
338 + という流れになります。
339 +
340 + `setup`、`buildStart`、`onConfigResolved`、Content 処理、`buildEnd` は、1つの `ContentManager` につき一度だけ実行されます。`buildEnd` は Diagnostics の収集後に完成した Manifest を受け取る唯一の終端 Hook です。`dispose` は確保した Resource の解放に使用し、解決済み Plugin の逆順で実行されます。
341 +
342 + Lifecycle の実行順は、解決済み Plugin Order に従います。
343 +
344 + Hook で Error が発生した場合は、
345 +
346 + - Plugin 名
347 + - Hook 名
348 + - 元の Cause
349 +
350 + が分かる形で上位へ伝播させてください。
351 +
352 + # 9. Content Lifecycle
353 +
354 + Content は複数の段階を通って処理されます。
355 +
356 + ```mermaid id="47k3qg"
357 + flowchart TD
358 + Config["Config Resolved"]
359 + Loaded["Content Loaded"]
360 + Location["Public Location Resolved"]
361 + Parsed["Post Parsed"]
362 + Processed["Post Processed"]
363 + Graph["Content Graph"]
364 + Manifest["Manifest Created"]
365 +
366 + Config --> Location
367 + Location --> Loaded
368 + Loaded --> Parsed
369 + Parsed --> Processed
370 + Processed --> Graph
371 + Graph --> Manifest
372 + ```
373 +
374 + 全体の順序は `setup` → `buildStart` → `onConfigResolved` → Public Location 解決 → `onContentLoaded` → Markdown / HTML Pipeline → `onPostParsed` → `onPostProcessed` → `extendContentGraph` → `onManifestCreated` → Diagnostics → `buildEnd` です。
375 +
376 + 代表的な Hook は、
377 +
378 + ```text id="6idgbj"
379 + onConfigResolved
380 + onContentLoaded
381 + onPostParsed
382 + onPostProcessed
383 + onManifestCreated
384 + ```
385 +
386 + です。
387 +
388 + 必要な段階の Hook だけを使用してください。
389 +
390 + たとえば Manifest にすでに存在する情報を `onContentLoaded` で独自に再構築する、といった実装は避けます。
391 +
392 + # 10. Markdown / HTML Pipeline
393 +
394 + Markdown や HTML の意味を変換する場合は remark / rehype を利用します。
395 +
396 + 単純な Plugin なら、
397 +
398 + ```ts id="twapvp"
399 + definePlugin({
400 + name: "example",
401 +
402 + remarkPlugins: [
403 + remarkExample,
404 + ],
405 +
406 + rehypePlugins: [
407 + rehypeExample,
408 + ],
409 + });
410 + ```
411 +
412 + と宣言できます。
413 +
414 + Pipeline 自体を構成する必要がある場合は、
415 +
416 + ```ts id="5fj1xn"
417 + definePlugin({
418 + name: "example",
419 +
420 + extendMarkdownPipeline(pipeline) {
421 + pipeline.use(remarkExample);
422 + },
423 +
424 + extendHtmlPipeline(pipeline) {
425 + pipeline.use(rehypeExample);
426 + },
427 + });
428 + ```
429 +
430 + を使用します。
431 +
432 + Markdown / HTML の意味変換を Application Component に持ち込まず、Plugin の Pipeline 処理として実装するのが基本です。
433 +
434 + # 11. Content Graph
435 +
436 + Content Graph を拡張する場合は `extendContentGraph` を使用します。
437 +
438 + ```mermaid id="kjw3kt"
439 + flowchart LR
440 + Manifest["Manifest"]
441 + Graph["Content Graph"]
442 + Plugin["extendContentGraph"]
443 + Result["Extended Graph"]
444 +
445 + Manifest --> Graph
446 + Graph --> Plugin
447 + Plugin --> Result
448 + ```
449 +
450 + Backlinks や Graph 系機能を実装するために、Plugin が Filesystem を再走査しないでください。
451 +
452 + すでに解決された Manifest / Content Graph を利用します。
453 +
454 + # 12. Public Location
455 +
456 + Content の公開 URL を変更する Plugin では `resolveContentLocations` を使用します。
457 +
458 + 最初に Core が Default Location を解決します。
459 +
460 + ```text id="dug8by"
461 + index
462 + → /
463 +
464 + その他
465 + → /{slug}
466 + ```
467 +
468 + その後、解決済み Plugin Order に従って Location Hook が実行されます。
469 +
470 + ```mermaid id="rbm5j4"
471 + flowchart LR
472 + Content["Content"]
473 + Default["Default Resolver"]
474 + PluginA["Plugin A"]
475 + PluginB["Plugin B"]
476 + Location["ContentPublicLocation"]
477 +
478 + Content --> Default
479 + Default --> PluginA
480 + PluginA --> PluginB
481 + PluginB --> Location
482 + ```
483 +
484 + 結果は `ContentPublicLocation` として、
485 +
486 + - Manifest
487 + - Content Graph
488 + - Markdown Pipeline
489 +
490 + などから利用されます。
491 +
492 + URL Strategy は Plugin が所有します。
493 +
494 + Core は特定 Plugin の URL 規則を知りません。
495 +
496 + Consumer は最終的な、
497 +
498 + ```ts id="b9m3p8"
499 + entry.permalink
500 + ```
501 +
502 + を利用します。
503 +
504 + Location を解決できない場合は、slug へ暗黙的に fallback せず明示的な Error とします。
505 +
506 + # 13. Renderers
507 +
508 + `renderers` は記事本文内の特殊な Content Target を HTML へ変換します。
509 +
510 + たとえば、
511 +
512 + ```text id="o4apio"
513 + Canvas
514 + Bases
515 + Excalidraw
516 + Attachment
517 + Media
518 + ```
519 +
520 + などです。
521 +
522 + Renderer Context には、
523 +
524 + ```text id="pbh0ss"
525 + kind
526 + path
527 + raw
528 + label
529 + url
530 + embed
531 + ```
532 +
533 + と通常の `PluginContext` が含まれます。
534 +
535 + ```mermaid id="m5n3av"
536 + flowchart TD
537 + Target["Content Target"]
538 + Renderer{"このRendererが処理する?"}
539 +
540 + Target --> Renderer
541 + Renderer -->|Yes| HTML["HTML"]
542 + Renderer -->|No| Next["次のRenderer"]
543 + ```
544 +
545 + 処理対象でなければ `null` を返します。
546 +
547 + これによって複数 Renderer が同じ Pipeline に参加できます。
548 +
549 + # 14. Page Types
550 +
551 + 独立した URL を持つ画面を Plugin が提供する場合は `pageTypes` を使用します。
552 +
553 + たとえば、
554 +
555 + ```text id="9tlwkd"
556 + /explore
557 + /report
558 + /tags/example
559 + ```
560 +
561 + などです。
562 +
563 + Page Type は、
564 +
565 + - 一意な `id`
566 + - SSG 用 `paths`
567 + - 必要に応じた `priority`
568 + - `resolve`
569 +
570 + を持ちます。
571 +
572 + Resolver は Framework 非依存の HTML Body または `null` を返します。
573 +
574 + 必要なら、
575 +
576 + ```text id="pjgd7n"
577 + title
578 + description
579 + headTags
580 + ```
581 +
582 + も返せます。
583 +
584 + ただし Document Frame は Site Application が所有します。
585 +
586 + ```mermaid id="dkbx6s"
587 + flowchart LR
588 + Plugin["Plugin Page Type"]
589 + Core["Core Resolver"]
590 + Integration["HonoX Integration"]
591 + Site["Site Document Frame"]
592 +
593 + Plugin --> Core
594 + Core --> Integration
595 + Integration --> Site
596 + ```
597 +
598 + Taxonomy の一覧や Explorer のような独立画面には Page Type を使用します。
599 +
600 + Canvas、Bases、Excalidraw のような記事本文への埋め込みには Renderer を使用します。
601 +
602 + Plugin が HonoX Route File や Document Frame を所有しないことが重要です。
603 +
604 + Page Type の詳しい仕組みは [Page System](./page-system.md) を参照してください。
605 +
606 + # 15. Assets
607 +
608 + Plugin 固有の Stylesheet は Plugin Package 内に置き、`assets` から公開します。
609 +
610 + ```ts id="b4qrvi"
611 + assets: [
612 + {
613 + pluginName: "example",
614 + kind: "style",
615 + moduleSpecifier:
616 + "@riebeckite/plugin-example/style.css",
617 + },
618 + ]
619 + ```
620 +
621 + Integration がこの Module Specifier を解決して Browser へ届けます。
622 +
623 + ```mermaid id="pbj8dg"
624 + flowchart LR
625 + CSS["Plugin style.css"]
626 + Asset["assets"]
627 + Integration["Integration"]
628 + Browser["Browser"]
629 +
630 + CSS --> Asset
631 + Asset --> Integration
632 + Integration --> Browser
633 + ```
634 +
635 + Plugin 固有 CSS を `apps/web` にコピーしたり、Browser から `/node_modules` を直接参照させたりしないでください。
636 +
637 + # 16. CSS Hooks
638 +
639 + 再利用可能な UI を Plugin が描画する場合は、最外要素に Stable Root Hook を付けます。
640 +
641 + Plugin / Feature Hook は、
642 +
643 + ```text id="59lz7m"
644 + rr-<feature>
645 + ```
646 +
647 + とします。
648 +
649 + たとえば、
650 +
651 + ```text id="gj2f5a"
652 + rr-search
653 + rr-callout
654 + rr-query
655 + rr-code
656 + ```
657 +
658 + です。
659 +
660 + 内部要素では BEM を利用できます。
661 +
662 + ```text id="9lq6se"
663 + rr-search
664 + rr-search__input
665 + rr-search__result
666 + rr-search--loading
667 + ```
668 +
669 + Namespace の役割は次のとおりです。
670 +
671 + | Namespace | 用途 |
672 + | --- | --- |
673 + | `rb-*` | Framework の構造 Hook |
674 + | `--rb-*` | Framework の Semantic Token |
675 + | `rr-*` | Plugin / Feature Hook |
676 + | `--rr-*` | Plugin 固有 Token |
677 +
678 + Plugin の出力を `rb-*` Namespace に置かないでください。
679 +
680 + 既存 Class がある場合は削除せず、`rr-*` Hook を追加します。
681 +
682 + また、
683 +
684 + ```text id="21g4wg"
685 + rr-feature__*
686 + rr-feature--*
687 + ```
688 +
689 + は原則として内部実装です。
690 +
691 + Theme から利用してよい子孫 Class だけを Public Hook として文書化してください。
692 +
693 + # 17. Client Entries
694 +
695 + Browser 上で初期化処理が必要な場合だけ `clientEntries` を使用します。
696 +
697 + ```ts id="a8cjlk"
698 + clientEntries: [
699 + {
700 + pluginName: "example",
701 + moduleSpecifier:
702 + "@riebeckite/plugin-example/client",
703 + exportName: "initExample",
704 +
705 + publicConfig: {
706 + selector: ".example",
707 + },
708 + },
709 + ]
710 + ```
711 +
712 + ```mermaid id="wqsskl"
713 + flowchart LR
714 + Plugin["Plugin"]
715 + Client["Client Entry"]
716 + Build["Integration"]
717 + Browser["Browser"]
718 +
719 + Plugin --> Client
720 + Client --> Build
721 + Build --> Browser
722 + ```
723 +
724 + SSR / Build-time だけで完結する Plugin に Client JavaScript を追加しないでください。
725 +
726 + `publicConfig` は Browser へ渡されるため、
727 +
728 + - Token
729 + - Credential
730 + - Secret
731 + - Private Service URL
732 +
733 + などを含めてはいけません。
734 +
735 + Plugin の `options` が自動的に Client へ渡されることもありません。
736 +
737 + # 18. Endpoints / SEO
738 +
739 + HTTP Endpoint を提供する場合は `endpoints` Contract を使用します。
740 +
741 + ```mermaid id="brq9cr"
742 + flowchart LR
743 + Plugin["Plugin"]
744 + Endpoint["Endpoint Contract"]
745 + Integration["Integration"]
746 + Router["Host Router"]
747 +
748 + Plugin --> Endpoint
749 + Endpoint --> Integration
750 + Integration --> Router
751 + ```
752 +
753 + HonoX など特定の Route Framework を Plugin 本体へ直接組み込まないでください。
754 +
755 + SEO へ参加する場合は `seo` を使用します。
756 +
757 + たとえば Metadata や Feed に Plugin が情報を追加できます。
758 +
759 + Application Route 側へ Plugin 固有 SEO Logic を再実装しないことが重要です。
760 +
761 + # 19. Diagnostics
762 +
763 + Plugin 固有の問題は `addDiagnostics` から報告します。
764 +
765 + ```ts id="1xq5te"
766 + addDiagnostics(context) {
767 + return [
768 + {
769 + // Diagnostic contract
770 + },
771 + ];
772 + }
773 + ```
774 +
775 + 診断結果は可能な限り Structured Data として返してください。
776 +
777 + Plugin が、
778 +
779 + ```ts id="c4x89a"
780 + console.log(...)
781 + ```
782 +
783 + で独自の CLI Output を作るのではなく、Diagnostics または Logger を使用します。
784 +
785 + # 20. Plugin Cache
786 +
787 + `context.cache` は Plugin ごとに分離された Build-time Cache です。
788 +
789 + 保存するデータには次の条件があります。
790 +
791 + - 再生成できる
792 + - JSON Serializable
793 + - Plugin Namespace 内で完結する
794 + - 壊れていても安全に Cache Miss として扱える
795 +
796 + `cacheVersion` を使って Cache Format の互換性を管理できます。
797 +
798 + Write は Atomic に行います。
799 +
800 + ```mermaid id="42mkl4"
801 + flowchart TD
802 + Work["Plugin Work"]
803 + Cache{"有効なCache?"}
804 +
805 + Work --> Cache
806 +
807 + Cache -->|Yes| Reuse["Reuse"]
808 + Cache -->|No| Generate["Regenerate"]
809 + ```
810 +
811 + これは Runtime Database ではありません。
812 +
813 + Cloudflare Workers などの永続 Storage として使用しないでください。
814 +
815 + # 21. Logger / Tracer
816 +
817 + Plugin では Framework の Logger / Tracer を利用できます。
818 +
819 + ```ts id="j7ad6k"
820 + context.logger.info("...");
821 +
822 + await context.tracer.span(
823 + "plugin.example.work",
824 + {
825 + plugin: "example",
826 + },
827 + async () => {
828 + // work
829 + },
830 + );
831 + ```
832 +
833 + Logger は処理内容を記録し、Tracer は処理時間などを Structured Trace として記録します。
834 +
835 + Profiler はこの Trace を利用するため、Plugin ごとに独自の Stopwatch や Profiling System を作る必要はありません。
836 +
837 + # 22. Site 内だけで使う Plugin
838 +
839 + Plugin は npm に公開しなくても利用できます。
840 +
841 + Site 内に、
842 +
843 + ```text id="17msvn"
844 + site/
845 + └─ extensions/
846 + └─ local-plugin.ts
847 + ```
848 +
849 + のように置いて `definePlugin()` できます。
850 +
851 + ```ts id="mqm2gv"
852 + // site/extensions/local-plugin.ts
853 +
854 + return definePlugin({
855 + name: "site-local",
856 +
857 + assets: [
858 + {
859 + pluginName: "site-local",
860 + kind: "style",
861 + moduleSpecifier:
862 + "/extensions/plugin.css",
863 + },
864 + ],
865 + });
866 + ```
867 +
868 + Site-local Plugin も Published Plugin と同じ、
869 +
870 + - Dependency Resolution
871 + - Pipeline
872 + - Hooks
873 + - Diagnostics
874 + - Renderer
875 + - Page Type
876 +
877 + などの Contract を利用します。
878 +
879 + `createStyleAsset()` と `createClientEntry()` は
880 + `@riebeckite/plugin-<name>/...` の specifier しか組み立てないため、その名前で
881 + ない package(未公開 Plugin、別名の公開 package)は Host Bundler が解決できる
882 + `moduleSpecifier` を直接指定してください。
883 +
884 + # 23. Package として配布する
885 +
886 + Plugin を再利用可能な Package として配布する場合は、たとえば次の構成にできます。
887 +
888 + ```text id="fs7fzp"
889 + packages/plugins/example/
890 + ├─ index.ts
891 + ├─ components/ # 必要な場合のみ
892 + ├─ client.ts # 必要な場合のみ
893 + ├─ src/
894 + │ ├─ remark.ts
895 + │ ├─ rehype.ts
896 + │ ├─ renderer.ts
897 + │ └─ types.ts
898 + ├─ style.css # 必要な場合のみ
899 + ├─ package.json
900 + ├─ README_ja.md
901 + └─ README.md
902 + ```
903 +
904 + Riebeckite repository 内では `packages/plugins/backlinks` が参考になります。
905 +
906 + 公開 package では Build 済み ESM と型定義を publish し、`exports` をその成果物へ
907 + 向け、`prepack` script で build します。Repository の build script は publish
908 + されないため、`esbuild`(`format: "esm"`、`packages: "external"`、
909 + `external: ["@riebeckite/*"]`)と `tsc --emitDeclarationOnly` による小さな build を
910 + 用意してください。最小構成の `package.json` は
911 + [Repository 外で Plugin を配布する](../reference/plugin-api.md#repository-外で-plugin-を配布する)
912 + を参照してください。
913 +
914 + ただし、すべての Plugin に `client.ts`、`style.css`、`components/` が必要なわけではありません。
915 +
916 + 必要なものだけを作成してください。
917 +
918 + # 24. Repository 外で配布する
919 +
920 + 外部 Plugin Package は Riebeckite monorepo の内部構造に依存させません。
921 +
922 + 基本的には、
923 +
924 + ```text id="8pn4j3"
925 + @riebeckite/core
926 + ```
927 +
928 + の Public API を利用します。
929 +
930 + Plugin 自身が持つ、
931 +
932 + ```text id="svimxk"
933 + ./client
934 + ./components
935 + ./style.css
936 + ```
937 +
938 + などは、自身の `package.json` の `exports` で公開します。
939 +
940 + 次のような Internal Import は使用しません。
941 +
942 + ```ts id="y6jy1n"
943 + import {
944 + something,
945 + } from "@riebeckite/core/src/...";
946 + ```
947 +
948 + monorepo 内にしか存在しない相対 Path にも依存しないでください。
949 +
950 + # 25. ESM
951 +
952 + NodeNext / ESM Package では、Build 後の JavaScript を Node.js が実際に解決できる必要があります。
953 +
954 + Development 時だけ TypeScript Loader が、
955 +
956 + ```text id="5k12um"
957 + extensionless import
958 + ```
959 +
960 + などを解決できている状態に依存しないでください。
961 +
962 + Package の正しさは Source Code だけではなく、**Build 後の配布形式でも確認する**必要があります。
963 +
964 + # 26. Plugin を検証する
965 +
966 + 実装後は、小さい範囲から順番に確認します。
967 +
968 + ```mermaid id="b4ivxl"
969 + flowchart LR
970 + Check["check"]
971 + Doctor["doctor"]
972 + Inspect["inspect plugins"]
973 + Build["build"]
974 +
975 + Check --> Doctor
976 + Doctor --> Inspect
977 + Inspect --> Build
978 + ```
979 +
980 + まず Configuration と Plugin Resolution を確認します。
981 +
982 + ```sh id="bgxy4d"
983 + pnpm exec riebeckite check
984 + ```
985 +
986 + 次に Project の Health を確認します。
987 +
988 + ```sh id="8bfj5k"
989 + pnpm exec riebeckite doctor
990 + ```
991 +
992 + 解決された Plugin を確認します。
993 +
994 + ```sh id="ad0h9x"
995 + pnpm exec riebeckite inspect plugins
996 + ```
997 +
998 + 最後に実際の生成物まで確認します。
999 +
1000 + ```sh id="f7grlr"
1001 + pnpm exec riebeckite build
1002 + ```
1003 +
1004 + Plugin が解決されない場合は、まず `check` の出力から、
1005 +
1006 + - Import Error
1007 + - Missing Capability
1008 + - Duplicate Provider
1009 + - Dependency Cycle
1010 + - Invalid Options
1011 +
1012 + などを確認してください。
1013 +
1014 + # 27. 実装前の確認
1015 +
1016 + Plugin を作り始める前に、最後に次の順番で考えると責務を分離しやすくなります。
1017 +
1018 + ```mermaid id="7p5i6d"
1019 + flowchart TD
1020 + Start["追加したい機能"]
1021 + Plugin{"再利用可能な機能?"}
1022 +
1023 + Start --> Plugin
1024 +
1025 + Plugin -->|No| Other{"何を変える?"}
1026 + Plugin -->|Yes| Point{"最小のExtension Pointは?"}
1027 +
1028 + Other -->|"見た目"| Theme["Theme"]
1029 + Other -->|"Site固有"| App["Application"]
1030 + Other -->|"Framework共通Model"| Core["Core"]
1031 + Other -->|"HonoX / Vite接続"| Integration["Integration"]
1032 +
1033 + Point --> Implement["Pluginとして実装"]
1034 + ```
1035 +
1036 + Plugin にすると決めた後も、
1037 +
1038 + **その機能に本当に必要な Extension Point だけを使用する**
1039 +
1040 + ことが重要です。
1041 +
1042 + Filesystem を再走査する前に Manifest や Content Graph が使えないか、独自 Route を追加する前に Page Type が使えないか、Client JavaScript を追加する前に SSR / Build-time だけで完結できないかを確認してください。
1043 +
1044 + これによって Plugin を小さく保ち、Core、Integration、Application との不要な結合を避けられます。
1045 +
1046 + ## 関連資料
1047 +
1048 + - [はじめてのプラグイン作成](../plugins/writing-a-plugin.md) — 最初の Plugin を作る
1049 + - [Plugin System](./plugin-system.md) — Plugin System 全体の考え方
1050 + - [Plugin API](../reference/plugin-api.md) — API Contract
1051 + - [Page System](./page-system.md) — 独立ページ
1052 + - [Content System](./content-system.md) — Manifest / Graph / Pipeline
1053 + - [Architecture](./architecture.md) — Core / Plugin / Integration / Theme / App の責務
1054 + - [Framework Reference](../reference/README.md) — Public API
1055 +