`gatsby build` をCLIからHTMLまで追う: ブートストラップパイプライン
前提知識
- ›第1回: アーキテクチャとモノレポの概要
- ›webpackの基本概念(ローダー、プラグイン)
- ›ビルドパイプラインの基礎知識
gatsby build をCLIからHTMLまで追う: ブートストラップパイプライン
モノレポ全体の構造を把握したところで、次は gatsby build の実行フローを最初から最後まで追ってみましょう。コマンドハンドラーが起動する瞬間から、最終的なHTMLファイルが public/ に書き出されるまでの全工程です。数十のファイルにまたがる長い旅路ですが、Gatsbyのすべての機能はこのパイプラインのいずれかのステージで表現されるため、全体像を把握しておくことが不可欠です。
ビルドコマンドのパイプライン
packages/gatsby/src/commands/build.ts のビルドコマンドハンドラーは、単一の非同期関数としてエクスポートされています。構造は線形で、各ステージが完了してから次のステージが始まります。
flowchart TD
A["bootstrap()"] --> B["onPreBuild hook"]
B --> C["writeOutRequires()"]
C --> D["buildProductionBundle()"]
D --> E["buildRenderer()"]
E --> F["preparePageTemplateConfigs()"]
F --> G["Build Rendering Engines"]
G --> H["calculateDirtyQueries()"]
H --> I["Run Queries"]
I --> J["buildHTMLPagesAndDeleteStaleArtifacts()"]
J --> K["onPostBuild hook"]
K --> L["adapterManager.adapt()"]
パイプラインは大きく3つのフェーズに分けられます。
- Bootstrap(127〜131行目): プラグインの初期化、データのソース取得、スキーマ構築、ページ生成、クエリ抽出
- Bundle(153〜295行目): JSバンドルとHTMLレンダラーを生成する4つのwebpackコンパイル
- Render(302〜511行目): クエリの実行、HTML生成、アダプターによるデプロイ
それぞれのフェーズを詳しく見ていきましょう。
Initializeサービス: 設定、テーマ、プラグイン
127行目の bootstrap() 呼び出しは、第1回で紹介したブートストラップオーケストレーターへと処理を委譲し、そこからさらに initialize() が呼ばれます。これはコードベース全体の中で最も複雑な関数です。実行順序を整理すると次のようになります。
sequenceDiagram
participant Build as build.ts
participant Bootstrap as bootstrap/index.ts
participant Init as services/initialize.ts
participant Parcel as compileGatsbyFiles
participant Config as load-config
participant Themes as load-themes
participant Plugins as load-plugins
Build->>Bootstrap: bootstrap({ program })
Bootstrap->>Init: initialize(context)
Init->>Parcel: compileGatsbyFiles(siteDirectory)
Note over Parcel: Compile gatsby-config.ts, gatsby-node.ts
Init->>Config: loadConfig({ siteDirectory })
Config->>Themes: loadThemes(config)
Note over Themes: Recursive theme resolution
Themes-->>Config: Merged config
Config-->>Init: Validated config
Init->>Plugins: loadPlugins(config, siteDirectory)
Note over Plugins: Normalize → Validate → Flatten
Init->>Init: startPluginRunner()
Init-->>Bootstrap: { store, workerPool }
Parcelによるコンパイル
Gatsbyが設定ファイルを読み込む前に、まずTypeScriptとESMのファイルをコンパイルする必要があります。initialize.tsの170〜173行目では、compileGatsbyFiles がParcelのバンドラーを使って gatsby-config.ts と gatsby-node.ts をNodeが require できるCommonJS形式に変換します。GatsbyのコンフィグをTypeScriptで書けるのは、他の処理が始まる前にParcelがトランスパイルを担ってくれるためです。
設定の読み込み
設定ローダー(packages/gatsby/src/bootstrap/load-config/index.ts)は次の3つの処理を行います。
- コンパイル済みの
gatsby-configファイルを読み込む handleFlagsを通じてフィーチャーフラグを処理するloadThemesを通じてテーマ解決に設定を渡す
ルートの設定はオブジェクト形式でなければならず、関数形式にはできません。関数形式が許されているのはテーマの設定のみです(テーマオプションを引数として受け取ります)。この区別は28〜36行目でバリデーションされています。
テーマの解決
packages/gatsby/src/bootstrap/load-themes/index.ts のテーマ解決は再帰的に行われます。テーマとは、独自の gatsby-config を持つGatsbyプラグインに過ぎません。テーマは他のテーマに依存でき、各テーマの設定は親の設定とマージされます。解決アルゴリズムは依存ツリーを深さ優先で走査し、すべてのテーマ設定を収集した上で下から上へとマージしていきます。
プラグインの読み込み
プラグインローダー(packages/gatsby/src/bootstrap/load-plugins/index.ts)はマージ済みの設定を受け取り、次の処理を順番に実行します。
- 正規化: 文字列形式のプラグイン参照を
{ resolve, options }オブジェクトに変換する - バリデーション: プラグインオプションをスキーマに対して検証する
- 内部プラグインの読み込み: Gatsbyに同梱されているプラグインを追加する
- フラット化: プラグインツリーを単一の配列に展開する
- 照合: どのプラグインがどのAPIを実装しているかを整理する
- エクスポートの検証: 既知のAPIリスト(
api-node-docs、api-browser-docs、api-ssr-docs)に照らしてエクスポートを確認する
最終的な出力はフラットな配列で、SET_SITE_FLATTENED_PLUGINS(79〜82行目)としてReduxにディスパッチされます。この配列が、すべてのプラグインとそれぞれが実装するAPIの正規レジストリとなります。
APIランナー: コアとプラグインをつなぐ橋
sourceNodes、createPages、onCreateWebpackConfig などのプラグインフックを呼び出す必要があるとき、Gatsbyは必ず packages/gatsby/src/utils/api-runner-node.js のAPIランナーを経由します。
APIランナーはGatsbyのプラグインアーキテクチャの中枢神経とも言える存在です。各API呼び出しに対して、次の処理を行います。
- フラット化されたプラグインリストから、要求されたAPIを実装しているプラグインを検索する
- 各プラグインに渡す豊富なコンテキストオブジェクトを構築する。内容は以下のとおり:
- バインドされたアクションクリエーター(
createNode、createPage、createRedirectなど) - データアクセス関数(
getNode、getNodes、getNodesByType) - ユーティリティ関数(
createNodeId、createContentDigest) - スキーマの型ビルダー(
buildObjectType、buildUnionTypeなど) - プラグインにスコープされた
cacheインスタンス - 構造化ログのための
reporter
- バインドされたアクションクリエーター(
- 各プラグインの実装を並列ではなく順番に呼び出す
sequenceDiagram
participant Core as Gatsby Core
participant Runner as api-runner-node
participant PluginA as gatsby-source-filesystem
participant PluginB as gatsby-transformer-remark
participant Redux as Redux Store
Core->>Runner: apiRunnerNode("sourceNodes", { parentSpan })
Runner->>Runner: Look up plugins implementing "sourceNodes"
Runner->>Runner: Construct context (actions, getNode, cache...)
Runner->>PluginA: sourceNodes(context)
PluginA->>Redux: createNode(fileNode)
PluginA-->>Runner: done
Runner->>PluginB: sourceNodes(context)
PluginB-->>Runner: done
Runner-->>Core: results
プラグインに渡されるアクションクリエーターは「二重バインド」されています。まずReduxに対して bindActionCreators でバインドされ、次に traceId、plugin.name、deferNodeMutation フラグなどのメタデータを注入するラッパーで特定のプラグインとAPI呼び出しにバインドされます(87〜100行目)。これにより、プラグインがディスパッチするすべてのアクションに発信元の情報が付与されます。
ヒント: プラグインを順番に実行するのは意図的な設計です。決定論的な動作を保証するためで、プラグインAの
sourceNodesは常にプラグインBより先に実行されます。あるプラグインが他のプラグインの作成したノードに依存している場合、この順序が重要になります。
4つのwebpackステージ
Gatsbyはそれぞれ異なる目的を持つ4つのwebpack設定を使用しています。packages/gatsby/src/utils/webpack.config.js のファクトリー関数は stage パラメーターを受け取り、それに対応した設定を生成します。
flowchart TD
subgraph "Development"
A["develop"] -->|"Hot reload, CSS injection"| A1["Browser bundle"]
B["develop-html"] -->|"No HMR, SSR target"| B1["HTML renderer"]
end
subgraph "Production"
C["build-javascript"] -->|"Minified, chunked"| C1["Browser JS/CSS"]
D["build-html"] -->|"Node target, SSR"| D1["HTML renderer"]
end
style A fill:#e8f5e9
style B fill:#e8f5e9
style C fill:#e1f5fe
style D fill:#e1f5fe
| ステージ | ターゲット | 用途 |
|---|---|---|
develop |
ブラウザ | React Fast RefreshとCSSホットリロードを備えた開発サーバー |
develop-html |
Node | 開発モード用SSRレンダラー(HMRプラグインなし) |
build-javascript |
ブラウザ | コード分割を含む本番用JSおよびCSSバンドル |
build-html |
Node | 静的生成用の本番HTMLレンダラー |
40〜44行目 のコメントは、この構成を簡潔に説明しています。
// Four stages or modes:
// 1) develop: for `gatsby develop` command, hot reload and CSS injection into page
// 2) develop-html: same as develop without react-hmre in the babel config for html renderer
// 3) build-javascript: Build JS and CSS chunks for production
// 4) build-html: build all HTML files
プラグインは onCreateWebpackConfig フックを使ってこれらの設定を変更できます。このフックには stage パラメーターが渡されるため、ステージごとに異なる修正を適用することが可能です。
ビルドコマンドでは本番用ステージのみが使用されます。160行目で buildProductionBundle が build-javascript ステージを実行し、続いて184行目で buildRenderer が build-html ステージを実行します。
クエリの実行とHTML生成
webpackのバンドルが完成すると、Gatsbyはクエリ実行フェーズに入ります。302行目の calculateDirtyQueries が、どのクエリを実行する必要があるかを判断します(インクリメンタルビルドでは変更されたクエリのみが実行されます)。
306〜308行目では重要な最適化が行われています。
queryIds.pageQueryIds = queryIds.pageQueryIds.filter(
query => getPageMode(query) === `SSG`
)
ビルド時にクエリが実行されるのはSSGページのみです。DSGページはクエリの実行を最初のリクエスト時まで先送りし、SSRページはリクエストのたびにクエリを実行します。これがレンダリングモードシステムの仕組みです。詳細は第5回で解説します。
マルチコアマシンでは、クエリはワーカープールで並列実行されます(330行目)。クエリが完了した後、507行目の buildHTMLPagesAndDeleteStaleArtifacts が最終的なHTMLファイルを生成します。
キャッシュ管理とビルドの永続化
ビルド全体を通じて、状態はLMDBを使って .cache/ ディレクトリに永続化されます。packages/gatsby/src/redux/index.ts のReduxストアでは、永続化する状態スライスが明示的に定義されています。
const persistedReduxKeys = [
`nodes`, `typeOwners`, `statefulSourcePlugins`, `status`,
`components`, `jobsV2`, `staticQueryComponents`,
`webpackCompilationHash`, `pageDataStats`, `pages`,
`staticQueriesByTemplate`, `pendingPageDataWrites`,
`queries`, `html`, `slices`, `slicesByTemplate`,
]
次のビルド時には、ストア作成時に readState() を通じてこれらのスライスがLMDBから読み戻されます。どのクエリが「ダーティ」でどのHTMLファイルが「古い」かをGatsbyが判断できるのは、現在の状態と永続化された状態を比較しているからです。
flowchart LR
subgraph ".cache/ Directory"
A["data/datastore (LMDB)"] -->|Nodes| B["Redux State"]
C["caches-lmdb/"] -->|Plugin caches| D["Per-plugin data"]
E["redux.state (deprecated)"] -.->|Legacy| B
end
subgraph "Build"
F["Previous Build State"] --> G["Calculate Dirty Queries"]
G --> H["Only rebuild changed pages"]
end
B --> F
133〜146行目の saveState() 関数は、永続化対象のキーを使って writeToCache を呼び出します。GATSBY_DISABLE_CACHE_PERSISTENCE というエスケープハッチが用意されている点も注目です。これは、サイト規模が非常に大きい場合にNode.jsの v8.serialize のバッファサイズ制限を回避するために追加されました。
アダプターとの統合
ビルドパイプラインの最終ステージはアダプターによるデプロイです。645〜648行目では次の処理が行われます。
if (adapterManager) {
await adapterManager.storeCache()
await adapterManager.adapt()
}
アダプターマネージャー(initialize.tsの189〜192行目で初期化時に生成)は、ビルド出力から RoutesManifest と FunctionsManifest を収集し、プラットフォームアダプターに渡します。アダプターはこれをデプロイ先プラットフォームのネイティブフォーマットに変換します。アダプターの詳細は第5回で取り上げます。
ヒント: ビルドが途中で失敗した場合、
.cache/ディレクトリに中途半端な状態が残ることがあります。gatsby cleanを実行すればディレクトリが完全に削除され、フルビルドが強制されます。キャッシュ関連の問題をデバッグする際の最終手段です。
次回予告
ここまで、ビルドパイプラインを一本のコンベアベルトとして追ってきました。しかし gatsby develop はさらに難しい課題に直面しています。リアルタイムで変更に反応しながら一貫性を保ち続けなければならないのです。次回は、開発サーバーを制御するXStateステートマシンに踏み込みます。これはコードベース全体の中でも最も際立ったアーキテクチャ上の特徴です。