Read OSS

開発者体験:Dev Server・i18n・設定システム

上級

前提知識

  • これまでの全記事の理解(モノレポ構造・プラグインライフサイクル・ビルドパイプライン・コンテンツプラグイン・テーマシステム)
  • ファイル監視(chokidar / fs.watch)の基本的な知識
  • i18n の基本概念(ロケール・翻訳ファイル)の理解

開発者体験:Dev Server・i18n・設定システム

これまでの 5 本の記事では、Docusaurus のモノレポ構造からプラグインライフサイクル、ビルドパイプライン、MDX 処理、テーマ解決まで順を追って追ってきました。最終回となる本記事では、運用レイヤーに焦点を当てます。Dev Server が高速なフィードバックループを実現する仕組み、i18n がロケールごとに独立したサイトをビルドする方法、TypeScript 設定を Joi バリデーションつきで読み込む柔軟な設定システム、そして段階的な移行を可能にする Future Flag の仕組みを解説します。最後に、アーキテクチャ全体をつなぐまとめのデータフロートレースで締めくくります。

Dev Server と Reloadable Site パターン

start.ts#L24-L64start コマンドは「reloadable site」を生成します。これはサイトの状態をラップし、reload()reloadPlugin() メソッドを提供する抽象レイヤーです。

const reloadableSite = await createReloadableSite({siteDirParam, cliOptions});

setupSiteFileWatchers(
  {props: reloadableSite.get().props, cliOptions},
  ({plugin}) => {
    if (plugin) {
      reloadableSite.reloadPlugin(plugin);
    } else {
      reloadableSite.reload();
    }
  },
);

reloadable site は変更の種類によって 2 つの処理を使い分けます。サイトレベルの変更(設定ファイルやローカライズディレクトリの変更)は reloadSite() によるフルリロードをトリガーし、プラグインレベルの変更(プラグインの監視パス内にあるコンテンツファイルの変更)は最適化された reloadSitePlugin() をトリガーします。

sequenceDiagram
    participant FS as File System
    participant W as Watcher
    participant RS as ReloadableSite
    participant SITE as site.ts

    FS->>W: Config file changed
    W->>RS: reload() (full)
    RS->>SITE: reloadSite() → loadSite()
    
    FS->>W: docs/intro.md changed
    W->>RS: reloadPlugin(docsPlugin)
    RS->>SITE: reloadSitePlugin()
    SITE->>SITE: Re-run only changed plugin's loadContent()
    SITE->>SITE: Re-run ALL plugins' allContentLoaded()
    SITE->>SITE: Regenerate .docusaurus/ files

site.ts#L310-L336reloadSitePlugin() の最適化は巧妙です。変更されたプラグインに対してのみ loadContent()contentLoaded() を再実行し、そのプラグインをプラグイン配列に差し替えたうえで、allContentLoaded()全プラグインに対して再実行します。これにより、プラグイン間のデータ整合性を保ちつつ、全プラグインのコンテンツを再読み込みするコストを回避しています。

ファイル監視のアーキテクチャ

watcher.ts#L96-L135 のウォッチャー設定は、サイト設定用と各プラグイン用の独立した chokidar ウォッチャーを生成します。

flowchart TD
    SETUP["setupSiteFileWatchers()"] --> SW["Site watcher<br/>siteConfigPath, localizationDir"]
    SETUP --> PW1["Plugin watcher: docs<br/>docs/**/*.md"]
    SETUP --> PW2["Plugin watcher: blog<br/>blog/**/*.md"]
    SETUP --> PW3["Plugin watcher: pages<br/>src/pages/**/*"]
    
    SW -->|change event| FULL["Full site reload"]
    PW1 -->|change event| P1["reloadPlugin(docs)"]
    PW2 -->|change event| P2["reloadPlugin(blog)"]
    PW3 -->|change event| P3["reloadPlugin(pages)"]

サイトウォッチャーは siteConfigPathlocalizationDir(翻訳ファイルの変更検知用)を監視します。各プラグインのウォッチャーは、plugin.getPathsToWatch() が返すパスを監視します。docs プラグインの場合、各バージョンのコンテンツディレクトリ内の全 Markdown ファイル、サイドバー設定ファイル、タグファイルが対象になります。

watcher.ts#L51-L66watch() 関数は chokidar を一貫した設定でラップしています。ignoreInitial: true により既存ファイルで起動時イベントが発生しないようにしつつ、--poll CLI フラグによるポーリングモードも用意されており、Docker on macOS のようにファイルシステムイベントが信頼できない環境にも対応しています。

Tip: カスタム Docusaurus プラグインを開発する際は、コンテンツディレクトリのグロブパターンを返す getPathsToWatch() を必ず実装しましょう。こうすることで、変更のたびにサイト全体を再ビルドするのではなく、プラグイン単位でのホットリロードが有効になります。

i18n のアーキテクチャ

Docusaurus の国際化には独自のアプローチがあります。各ロケールを完全に独立したサイトとしてビルドするのです。ランタイムでのロケール切り替えは行わず、ロケールごとに異なる静的出力を生成し、それぞれ別の URL パス(例:/fr/docs/intro)にデプロイします。

i18n.ts#L129-L215 の i18n 読み込みは、loadContext() の実行中に現在のロケールの i18n 設定を完全に構築します。

flowchart TD
    CONFIG["i18n config from docusaurus.config.js"] --> LOCALES["getLocaleList()"]
    LOCALES --> EACH["For each locale:"]
    EACH --> DEFAULT["getDefaultLocaleConfig()"]
    DEFAULT --> |"Intl API"| LABEL["Infer label, direction, calendar"]
    EACH --> MERGE["Merge with user localeConfigs"]
    MERGE --> TRANSLATE["Infer translate flag<br/>(i18n/locale/ dir exists?)"]
    TRANSLATE --> BASEURL["Compute locale baseUrl"]
    BASEURL --> I18N["Final I18n object"]

ロケール設定の推論は完全に自動です。89〜111 行目の getDefaultLocaleConfig() 関数は JavaScript の Intl API を使い、BCP 47 ロケールコードから表示ラベル・テキスト方向(LTR/RTL)・カレンダーシステム・HTML の lang 属性を自動的に導出します。たとえば ar を渡すだけで direction: 'rtl' が設定され、アラビア語のラベルも付与されます。

翻訳ファイルは i18n/<locale>/ ディレクトリ構造に配置します。記事 2 で見たプラグインライフサイクルの中で、translatePluginContent() 関数が翻訳ファイルを読み込み、各プラグインの translateContent() および translateThemeConfig() フックに渡します。ナビゲーションバーのアイテムやフッターのリンクなど、テーマの UI 文字列もここで翻訳されます。

コード翻訳 ― 「Next page」「Search」「Table of Contents」といったテーマコンポーネントの UI 文字列 ― は translations.ts で読み込まれる codeTranslations.json ファイルが担います。各プラグインは getDefaultCodeTranslationMessages() を実装することで、自身の UI 文字列に対するデフォルト翻訳を提供することもできます。

設定の読み込みとバリデーション

config.ts の設定読み込みシステムは幅広いファイル形式に対応しています。19〜36 行目の findConfig() 関数は以下の順序で設定ファイルを探索します。

docusaurus.config.ts → .mts → .cts → .js → .mjs → .cjs

TypeScript 設定はファーストクラスのサポートです。Docusaurus は @docusaurus/utilsloadFreshModule() を使い、TypeScript のトランスパイル・ESM インポート・モジュールキャッシュを一括で処理します。

読み込んだ設定はプレーンオブジェクトでも関数(async 関数を含む)でもかまいません。

const importedConfig = await loadFreshModule(siteConfigPath);
const loadedConfig =
  typeof importedConfig === 'function'
    ? await importedConfig()
    : await importedConfig;

つまり、環境変数の読み取り・リモートデータの取得・非同期の初期化処理を行ってから設定オブジェクトを返すファクトリ関数として設定ファイルを書くことができます。

読み込み後、設定は configValidation.ts#L654-L689validateConfig() を通じて Joi による包括的なバリデーションを受けます。417〜566 行目のスキーマは全フィールドを適切なデフォルト値つきで検証します。未知のフィールドは customFields の使用を提案するメッセージとともに報告されます。

flowchart TD
    FILE["docusaurus.config.ts"] --> LOAD["loadFreshModule()"]
    LOAD --> FUNC{Function?}
    FUNC -->|yes| CALL["await config()"]
    FUNC -->|no| OBJ["Use as-is"]
    CALL --> VALIDATE["validateConfig() via Joi"]
    OBJ --> VALIDATE
    VALIDATE --> POST["postProcessDocusaurusConfig()"]
    POST --> FINAL["DocusaurusConfig"]

570〜651 行目の postProcessDocusaurusConfig() は設定フラグ間の複雑な相互作用を処理します。たとえば v4.siteStorageNamespacing に基づいて storage.namespace を解決し、v4.fasterByDefault に基づいて個々の faster フラグを解決します。また v4.mdx1CompatDisabledByDefault に基づいて mdx1Compat フラグも解決します。フラグの依存関係も検証され、ssgWorkerThreadsremoveLegacyPostBuildHeadAttribute を、rspackPersistentCacherspackBundler をそれぞれ必要とします。

Future Flag システム

Future Flag システムは、後方互換性を保ちながら進化するための Docusaurus のアプローチを体現しているので、改めて詳しく見てみましょう。フラグは 2 つのグループに分かれています。

future.faster ― パフォーマンス最適化フラグ群。各フラグのデフォルトは false(76〜86 行目)で、faster: true と指定すると全フラグを一括で有効化できます(89〜99 行目)。v4.fasterByDefault フラグを有効にすると、未設定の faster フラグのデフォルトが true になります。

future.v4 ― v4 の破壊的変更との前方互換フラグ群。各フラグのデフォルトは false(101〜107 行目)で、v4: true で全フラグを有効化できます(110〜116 行目)。

graph TD
    subgraph "future.faster"
        SWC_JS[swcJsLoader]
        SWC_MIN[swcJsMinimizer]
        SWC_HTML[swcHtmlMinimizer]
        LCSS[lightningCssMinimizer]
        MDX_CACHE[mdxCrossCompilerCache]
        RSPACK[rspackBundler]
        RSPACK_CACHE[rspackPersistentCache]
        SSG_WORKER[ssgWorkerThreads]
        GIT[gitEagerVcs]
    end
    subgraph "future.v4"
        V4_HEAD[removeLegacyPostBuildHeadAttribute]
        V4_CSS[useCssCascadeLayers]
        V4_STORAGE[siteStorageNamespacing]
        V4_FASTER[fasterByDefault]
        V4_MDX1[mdx1CompatDisabledByDefault]
    end
    V4_FASTER -.->|"sets default for"| SWC_JS
    V4_FASTER -.->|"sets default for"| RSPACK
    SSG_WORKER -.->|"requires"| V4_HEAD
    RSPACK_CACHE -.->|"requires"| RSPACK

Tip: 推奨される移行手順は、まず future: {faster: true} を設定してパフォーマンスの恩恵をすぐに受け、v4 の全挙動を採用する準備ができたら v4: true を追加するというものです。どちらも boolean のショートカットか、フラグを個別指定するオブジェクト形式で記述できます。

まとめ:Docusaurus の全データフロー

docusaurus.config.ts から始まり、レンダリングされた HTML ページに至るまでの一連のリクエストを追ってみましょう。これまでの 6 本の記事すべてがここでつながります。

sequenceDiagram
    participant USER as docusaurus build
    participant CONFIG as Config (Article 6)
    participant PLUGINS as Plugin Lifecycle (Article 2)
    participant MDX as MDX Pipeline (Article 4)
    participant CODEGEN as Code Generation (Article 1)
    participant THEME as Theme Aliases (Article 5)
    participant BUNDLE as Bundler (Article 3)
    participant SSG as SSG (Article 3)

    USER->>CONFIG: loadSiteConfig()
    CONFIG->>CONFIG: Load .ts, validate Joi schema
    CONFIG->>PLUGINS: loadContext → loadPlugins()
    PLUGINS->>PLUGINS: Init plugins, expand presets
    PLUGINS->>MDX: docs plugin loadContent()
    MDX->>MDX: Scan .md files, parse front matter
    PLUGINS->>PLUGINS: contentLoaded() → addRoute(@theme/DocItem)
    PLUGINS->>CODEGEN: generateSiteFiles()
    CODEGEN->>CODEGEN: Write routes.js, registry.js, globalData.json
    CODEGEN->>BUNDLE: Webpack/Rspack compile
    BUNDLE->>MDX: MDX loader processes .md files
    BUNDLE->>THEME: Resolve @theme/* aliases
    BUNDLE->>BUNDLE: Produce client + server bundles
    BUNDLE->>SSG: executeSSG()
    SSG->>SSG: Render each route to HTML
    SSG->>USER: Static files in build/

同じ流れを文章で説明すると次のようになります。

  1. 設定の読み込みdocusaurus.config.ts を読み込みます。非同期関数のエクスポートに対応し、Joi によるバリデーションを実施します。

  2. コンテキストの生成 ― i18n ロケールを解決し、バンドラーを決定し、コード翻訳を読み込みます。

  3. プラグインの初期化 ― プリセットを個々のプラグインとテーマに展開し、設定を正規化してオプションをバリデーションし、コンストラクターを実行します。自己無効化するプラグインは null を返してフィルタリングされます。

  4. コンテンツの読み込み ― 全プラグインの loadContent() を並列実行します。docs プラグインは Markdown ファイルをスキャンし、バージョンを読み込み、メタデータを構築します。ロケールが必要とする場合はコンテンツが翻訳されます。

  5. ルートの生成 ― 各プラグインは actions オブジェクトを受け取ります。プラグインはテーマコンポーネントへの参照(@theme/DocItem など)を指定して addRoute() を呼び出し、createData() で JSON モジュールを書き出し、setGlobalData() でコンポーネント間共有データを設定します。

  6. プラグイン間の連携allContentLoaded() を実行し、プラグインが互いのコンテンツを参照して追加のルートを生成できるようにします。

  7. コード生成.docusaurus/ ディレクトリに、ルート・レジストリ・グローバルデータ・設定・翻訳・クライアントモジュールを書き出します。

  8. Webpack/Rspack のコンパイル ― クライアントバンドルとサーバーバンドルをビルドします。コンパイル中、MDX ローダーが Markdown ファイルを remark/rehype チェーンで処理し、テーマエイリアスシステムが @theme/* インポートを実際のコンポーネントファイルに解決します。

  9. 静的サイト生成 ― サーバーバンドルを読み込んで各ルートを HTML にレンダリングし、リンクとアンカーをバリデーション用に収集します。

  10. ビルド後処理 ― プラグインの postBuild() フックを実行し、サイト全体にわたってリンク切れを検証します。

最終的な成果物は build/ ディレクトリです。静的 HTML ファイルとして出力され、ブラウザ上でコード分割・ルートのプリロード・モダンなドキュメントフレームワークに期待されるすべての開発者体験機能を備えた React SPA としてハイドレートされます。

おわりに

Docusaurus のアーキテクチャは、層を重ねた抽象化の好例です。設定バリデーション・プラグインライフサイクル・コンテンツ処理・テーマ解決・バンドラー抽象化・SSG 実行 ― それぞれのレイヤーが明確に分離され、層間のインターフェースも明確に定義されています。.docusaurus/ の生成ディレクトリはその要となるもので、サーバー側の世界がクライアント側の世界に必要なすべてのものをシリアライズする場所です。

このコードベースは丁寧に読む価値があります。Plugin から InitializedPlugin、そして LoadedPlugin へと変化する型の流れは、ランタイムのステートマシンを的確に表現しています。3 つの名前空間によるエイリアスシステム(@theme@theme-original@theme-init)は、フォークすることなくコンポーネントをカスタマイズする手段を提供しています。future.fasterfuture.v4 のフラグは、既存ユーザーへの影響を最小限に抑えながらフレームワークを進化させる方法を示しています。

Docusaurus プラグインの開発、テーマのカスタマイズ、ビルドの問題のデバッグ、あるいは単にフレームワークの内部構造への好奇心。いずれの目的であっても、ここで紹介したアーキテクチャパターンは広く応用できます。ライフサイクルベースのプラグインシステム、通信の橋渡しとしてのコード生成、エイリアスベースのコンポーネント解決は、ドキュメントサイトをはるかに超えた普遍的な設計手法です。