Read OSS

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 builddocusaurus 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_DIRDOCUSAURUS_CLI_CONFIG を使えば、CLI 引数を渡さずにサイトディレクトリとコンフィグパスを上書きできます。これが必要な理由は、Commander.js が解析前にサイトディレクトリを特定できないという制約にあります。コンフィグのコンテキストを必要とする plugin の CLI 拡張において、この制約は鶏と卵の問題を引き起こすため、環境変数による回避策が設けられています。

loadSite() パイプライン

packages/docusaurus/src/server/site.ts#L276-L298 にある loadSite() 関数は、コードベース全体で最も重要な関数です。サイトデータが必要なコマンド——buildstartdeploy——はすべてこの関数を呼び出します。その処理の流れを見ていきましょう。

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 つのステージで構成されています。

  1. loadContext()(81〜173 行目):サイトコンフィグの読み込み、i18n ロケール設定の解決、出力ディレクトリの決定、bundler(Webpack または Rspack)の初期化、コード翻訳のロードを担います。

  2. loadPlugins():plugin ライフサイクルの 4 フェーズ——初期化、loadContent()contentLoaded()allContentLoaded()——を順に実行します。ロード済み plugin、ルート、グローバルデータを返します。詳細は第 2 回で解説します。

  3. createSiteProps()(175〜230 行目):plugin の実行結果を、ルート・メタデータ・HTML タグ・コード翻訳を含む統合 Props オブジェクトにまとめます。重複ルートの検出もここで行います。

  4. createSiteFiles()(233〜268 行目):generateSiteFiles() を呼び出して .docusaurus/ ディレクトリを書き出します。

ヒント: Props 型は、サーバーパイプラインとその後続処理(コード生成、bundler 設定、開発サーバー)をつなぐ「契約」です。ビルドに問題が生じたときは、まず Props の中身を確認するのが定石です。

.docusaurus/ という橋:コード生成

生成される .docusaurus/ ディレクトリは、サーバーとクライアントをつなぐ契約書です。packages/docusaurus/src/server/codegen/codegen.ts#L162-L174generateSiteFiles() 関数が、すべてのファイルを並列で書き出します。

生成ファイル 用途
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()(開発モードの場合)を呼び出します。BrowserRouterHashRouter の使い分けは、コンフィグの future.experimental_router オプションで制御されます。

SSR/SSG 向けには serverEntry.tsx が同じ <App />StaticRouterHelmetProvider、そして BrokenLinksProvider でラップします。BrokenLinksProvider はページ上のすべてのリンクとアンカーを収集し、ビルド後の検証に活用します。HTML にレンダリングしたうえで、マークアップとともに収集したメタデータを返します。

次のステップ

これで全体像をつかめました。モノレポの構成、サーバー/クライアントの分離、CLI のコマンドディスパッチ、loadSite() パイプライン、.docusaurus/ という橋、そしてクライアントのコンポーネントツリーを理解できたはずです。次回はサーバーパイプラインの核心部分、すなわちディスク上のコンテンツをブラウザの React ルートへと変換する 4 フェーズの plugin ライフサイクルを詳しく掘り下げていきます。