Docusaurus のアーキテクチャ:モノレポ全体像マップ
前提知識
- ›React の基礎知識(コンポーネント、フック、コンテキスト)
- ›Node.js および npm/yarn workspaces の使用経験
- ›静的サイトジェネレーターの一般的な理解
Docusaurus のアーキテクチャ:モノレポ全体像マップ
Docusaurus は React Native や Jest、Supabase をはじめ、数万ものドキュメントサイトを支えています。しかし、その内部構造を深く見たことがある開発者はほとんどいないでしょう。実態は、Lerna で管理された 40 パッケージ構成の Yarn workspaces モノレポです。アーキテクチャの核心は、サーバーサイドの Node.js オーケストレーションとクライアントサイドの React レンダリングを明確に分離した設計にあります。この分離を理解することが、コードベース全体を読み解く鍵となります。
本記事では、Docusaurus を理解するためのメンタルモデルを構築します。パッケージの分類から始まり、2 つの実行環境、CLI の仕組み、そして loadSite() パイプラインがコンフィグからコード生成へ至る流れを順に追っていきます。読み終える頃には、Docusaurus のどんな動作を調べたいときでも、迷わず目的の場所へたどり着けるようになるはずです。
モノレポの構成とパッケージの分類
ルートの package.json では、packages/* や website などを対象とした Yarn v1 workspaces が宣言されています。バージョン管理とパッケージ公開は Lerna (lerna.json) が統一バージョン(現在は 3.9.2)で一括管理します。
約 40 のパッケージは、以下のカテゴリに整理できます。
| カテゴリ | 例 | 役割 |
|---|---|---|
| Core | docusaurus |
CLI、サーバーパイプライン、クライアントアプリ、SSG |
| Bundler | docusaurus-bundler |
Webpack/Rspack の抽象化レイヤー |
| Content plugins | plugin-content-docs, plugin-content-blog, plugin-content-pages |
ファイル読み込みとルート生成 |
| Themes | theme-classic, theme-common |
React UI コンポーネント |
| Preset | preset-classic |
plugin と theme をまとめて提供 |
| MDX | mdx-loader |
MDX コンパイル用 Webpack loader |
| Utilities | utils, utils-common, utils-validation |
共有ヘルパー群 |
| Types | docusaurus-types |
TypeScript 型定義 |
| Scaffolding | create-docusaurus |
プロジェクト初期化ツール |
| Logger | docusaurus-logger |
構造化ロギング |
graph TD
subgraph "Preset Classic"
PC[preset-classic]
end
subgraph "Content Plugins"
DOCS[plugin-content-docs]
BLOG[plugin-content-blog]
PAGES[plugin-content-pages]
end
subgraph "Theme Layer"
TC[theme-classic]
TCM[theme-common]
end
subgraph "Core"
CORE[docusaurus]
BUNDLER[docusaurus-bundler]
MDX[mdx-loader]
end
PC --> DOCS
PC --> BLOG
PC --> PAGES
PC --> TC
TC --> TCM
DOCS --> MDX
BLOG --> MDX
CORE --> BUNDLER
docusaurus core パッケージは、全パッケージの中でも際立って規模が大きいです。CLI、サーバーサイドのパイプライン全体、クライアント React アプリ、SSG エンジン、webpack 設定レイヤーがすべてここに含まれています。OS のカーネルのようなものと考えると分かりやすいでしょう——他のすべてはこのコアに接続する形で動作します。
ヒント: コードベースを読み進めるときは、まず
packages/docusaurus/src/を起点にしましょう。サーバーサイドのコードはserver/とcommands/に、クライアントサイドのコードはclient/にあります。このディレクトリの分割を頭に入れておくことが、最初の重要なステップです。
2 つの世界:サーバーサイドとクライアントサイド
Docusaurus を読み解くうえで、まず理解しなければならない根本的な設計があります。サーバーサイド(Node.js)とクライアントサイド(ブラウザ上の React)は、生成ファイルを介して連携する独立したコードベースです。
flowchart LR
subgraph "Server World (Node.js)"
CONFIG[Config Loading]
PLUGINS[Plugin Lifecycle]
CODEGEN[Code Generation]
end
subgraph ".docusaurus/"
GEN[Generated Files]
end
subgraph "Client World (React)"
APP[App Component]
ROUTES[Routes]
HYDRATION[Hydration]
end
CONFIG --> PLUGINS --> CODEGEN --> GEN
GEN --> APP
GEN --> ROUTES
ROUTES --> HYDRATION
サーバーサイドのコードは docusaurus build や docusaurus start の実行時に動作します。コンフィグファイルの読み込み、plugin ライフサイクルの実行、ルートマニフェストの生成、SSG による静的 HTML の生成を担います。このコードは packages/docusaurus/src/server/ と packages/docusaurus/src/commands/ に置かれています。
クライアントサイドのコードは、ブラウザ上で hydrate される React アプリケーションです。ナビゲーションには React Router を使用し、ルートコンポーネントを遅延ロードしながら、テーマのコンテキスト管理を行います。このコードは packages/docusaurus/src/client/ にあります。
2 つの世界をつなぐ橋が .docusaurus/ ディレクトリです。サーバーサイドが書き出した JavaScript モジュール、JSON データ、ルート設定ファイルが格納されており、クライアントサイドの webpack ビルドが @generated/* エイリアス経由でこれらを読み込みます。サーバーが書き、クライアントが読む——シンプルな役割分担です。
CLI はオーケストレーター
CLI のエントリーポイントは packages/docusaurus/src/commands/cli.ts で、Commander.js を使ってすべてのコマンドを定義しています。runCLI() 関数がプログラムを生成し、引数を解析します。
flowchart TD
CLI[runCLI] --> BUILD[build]
CLI --> START[start]
CLI --> SWIZZLE[swizzle]
CLI --> DEPLOY[deploy]
CLI --> SERVE[serve]
CLI --> CLEAR[clear]
CLI --> WT[write-translations]
CLI --> WHI[write-heading-ids]
CLI --> EXT{External?}
EXT -->|Yes| PLUGIN_CMD[Plugin CLI Extensions]
各コマンドは専用のモジュールに対応しています。build は静的ビルドパイプライン全体を起動し、start はホットリロード付きの開発サーバーを立ち上げ、swizzle はテーマコンポーネントのカスタマイズを処理します。
注目すべき細かな動作が 1 つあります。CLI は cli.ts#L26-L40 でコマンドが「内部」かどうかを判定します。認識できないコマンドの場合は、解析前に externalCommand() を呼び出します。これが plugin 独自の CLI コマンドを登録する仕組みです。たとえば docs plugin はこの仕組みを使って、ドキュメントのスナップショットを作成する docs:version コマンドを追加しています。
53〜56 行目の環境変数にも注目してください。DOCUSAURUS_CLI_SITE_DIR と DOCUSAURUS_CLI_CONFIG を使えば、CLI 引数を渡さずにサイトディレクトリとコンフィグパスを上書きできます。これが必要な理由は、Commander.js が解析前にサイトディレクトリを特定できないという制約にあります。コンフィグのコンテキストを必要とする plugin の CLI 拡張において、この制約は鶏と卵の問題を引き起こすため、環境変数による回避策が設けられています。
loadSite() パイプライン
packages/docusaurus/src/server/site.ts#L276-L298 にある loadSite() 関数は、コードベース全体で最も重要な関数です。サイトデータが必要なコマンド——build、start、deploy——はすべてこの関数を呼び出します。その処理の流れを見ていきましょう。
sequenceDiagram
participant CLI as CLI Command
participant LS as loadSite()
participant LC as loadContext()
participant LP as loadPlugins()
participant CSP as createSiteProps()
participant CSF as createSiteFiles()
CLI->>LS: loadSite(params)
LS->>LC: Load config, i18n, bundler
LC-->>LS: LoadContext
LS->>LP: Run plugin lifecycle (4 phases)
LP-->>LS: plugins, routes, globalData
LS->>CSP: Merge routes, metadata, translations
CSP-->>LS: Props
LS->>CSF: Generate .docusaurus/ files
CSF-->>LS: Site ready
パイプラインは 4 つのステージで構成されています。
-
loadContext()(81〜173 行目):サイトコンフィグの読み込み、i18n ロケール設定の解決、出力ディレクトリの決定、bundler(Webpack または Rspack)の初期化、コード翻訳のロードを担います。 -
loadPlugins():plugin ライフサイクルの 4 フェーズ——初期化、loadContent()、contentLoaded()、allContentLoaded()——を順に実行します。ロード済み plugin、ルート、グローバルデータを返します。詳細は第 2 回で解説します。 -
createSiteProps()(175〜230 行目):plugin の実行結果を、ルート・メタデータ・HTML タグ・コード翻訳を含む統合Propsオブジェクトにまとめます。重複ルートの検出もここで行います。 -
createSiteFiles()(233〜268 行目):generateSiteFiles()を呼び出して.docusaurus/ディレクトリを書き出します。
ヒント:
Props型は、サーバーパイプラインとその後続処理(コード生成、bundler 設定、開発サーバー)をつなぐ「契約」です。ビルドに問題が生じたときは、まずPropsの中身を確認するのが定石です。
.docusaurus/ という橋:コード生成
生成される .docusaurus/ ディレクトリは、サーバーとクライアントをつなぐ契約書です。packages/docusaurus/src/server/codegen/codegen.ts#L162-L174 の generateSiteFiles() 関数が、すべてのファイルを並列で書き出します。
| 生成ファイル | 用途 |
|---|---|
docusaurus.config.mjs |
クライアントからアクセスするためのシリアライズ済みサイトコンフィグ |
routes.js |
ComponentCreator による遅延ロードを使った React Router のルートツリー |
registry.js |
コード分割のためのチャンク名とモジュールパスのマッピング |
routesChunkNames.json |
ルートパスと各ルートのモジュールに対応するチャンク名の対応表 |
client-modules.js |
plugin のクライアントモジュール(CSS や JS のサイドエフェクト) |
globalData.json |
useGlobalData() 経由でアクセスできる plugin 横断データ |
i18n.json |
現在のロケール設定 |
codeTranslations.json |
UI 文字列の翻訳データ |
site-metadata.json |
plugin バージョンとサイトメタデータ |
flowchart TD
GEN[generateSiteFiles] --> WARN[DONT-EDIT-THIS-FOLDER]
GEN --> CM[client-modules.js]
GEN --> SC[docusaurus.config.mjs]
GEN --> RF[routes.js + registry.js + routesChunkNames.json]
GEN --> GD[globalData.json]
GEN --> SM[site-metadata.json]
GEN --> I18N[i18n.json]
GEN --> CT[codeTranslations.json]
設計上の注目ポイントが 1 つあります。client module は import() ではなく require() を使っています。codegen.ts#L68-L77 のコメントにその理由が書かれています——import() は非同期ですが、client module には CSS が含まれることがあり、ロード順序が CSS の詳細度に直結するからです。同期的な require() を使うことで、CSS ファイルがバンドル内で正しい順序で読み込まれることを保証しています。
codegenRoutes.ts のルート生成ロジックも興味深い箇所です。ここでは 3 つのファイルを生成します。routes.js には遅延ロード用の ComponentCreator を使った最小限の React Router 設定が入ります。registry.js はチャンク名から webpack のマジックコメント付き動的 import() 呼び出しへのマッピングを提供します。routesChunkNames.json はルートパスとチャンク名を結びつけます。この 3 ファイル構成によって積極的なコード分割が実現し、各ページは必要な JavaScript だけを読み込めます。
クライアントのコンポーネントツリーとルーティング
クライアント React アプリケーションは packages/docusaurus/src/client/App.tsx で組み立てられます。コンポーネントツリーはマトリョーシカのように provider が入れ子になった構造です。
graph TD
EB[ErrorBoundary] --> DCP[DocusaurusContextProvider]
DCP --> BCP[BrowserContextProvider]
BCP --> ROOT["Root (@theme/Root)"]
ROOT --> TP["ThemeProvider (@theme/ThemeProvider)"]
TP --> SMD[SiteMetadataDefaults]
TP --> SM["SiteMetadata (@theme/SiteMetadata)"]
TP --> BIB[BaseUrlIssueBanner]
TP --> AN[AppNavigation]
AN --> PN[PendingNavigation]
PN --> ROUTES["renderRoutes(@generated/routes)"]
@theme/Root と @theme/ThemeProvider はテーマエイリアスシステムを通じて解決されます——theme-classic の実装か、ユーザーが swizzle したバージョンのどちらかを指します。@generated/routes のインポートは、サーバーが生成したルートファイルに接続されています。
clientEntry.tsx はブラウザのエントリーポイントで、hydration とクライアントサイドレンダリングの両方に対応しています。レンダリング前に現在のパスのルートデータをプリロードし、その後 ReactDOM.hydrateRoot()(SSG 済みページの場合)か ReactDOM.createRoot()(開発モードの場合)を呼び出します。BrowserRouter と HashRouter の使い分けは、コンフィグの future.experimental_router オプションで制御されます。
SSR/SSG 向けには serverEntry.tsx が同じ <App /> を StaticRouter、HelmetProvider、そして BrokenLinksProvider でラップします。BrokenLinksProvider はページ上のすべてのリンクとアンカーを収集し、ビルド後の検証に活用します。HTML にレンダリングしたうえで、マークアップとともに収集したメタデータを返します。
次のステップ
これで全体像をつかめました。モノレポの構成、サーバー/クライアントの分離、CLI のコマンドディスパッチ、loadSite() パイプライン、.docusaurus/ という橋、そしてクライアントのコンポーネントツリーを理解できたはずです。次回はサーバーパイプラインの核心部分、すなわちディスク上のコンテンツをブラウザの React ルートへと変換する 4 フェーズの plugin ライフサイクルを詳しく掘り下げていきます。