🙄
bun devでNext.jsが実行される仕組みを調べる
BunのランタイムにNextサーバーを起動するモードが用意されている。これが何なのか気になったので動作とソースコードを見比べて観察してみた。
- bun devがHTTPサーバーを起動するためのコマンドになっている
- bun自体がもともとdev-serverを高速化する目的で作られたためだと思われる
- Nextを使うかどうかはbunfig.tomlの設定ファイルで指定されるが、bun --use next で明示もできる
bunfig.toml
framework = "next"
- bun bun --use next で ./node_modules.bun, ./node_modules.server.bun の2つのバイナリを生成
- bun devで これらの.bun を実行する。--bunfile, --server-bunfile で指定もできる
- 設定ファイルで指定せずオプションなしの
bun dev
だと routeが解決されず 404エラーになる - なのでこのフラグをオンにした時にどういう処理を追加しているのかを見ていく
dev_command.zig
const Server = @import("../http.zig").Server;
pub const DevCommand = struct {
pub fn exec(ctx: Command.Context) !void {
Global.configureAllocator(.{ .long_running = true });
try Server.start(ctx.allocator, ctx.args, @TypeOf(ctx.debug), ctx.debug);
}
};
-
Global.configureAllocator
がマジックぽくて何をやっているのか不明 - global.zig と __global.zig が定義されていてどこかで実装が差し交わっている?
- Serverの実装がhttp.zigにあるのでそれを見ていく
http.zig
- HTTPサーバーのServer構造体+Handler関数でリクエストを処理する
エントリーポイント Server.start()
pub fn start(allocator: std.mem.Allocator, options: Api.TransformOptions, comptime DebugType: type, debug: DebugType) !void
- 第二引数 options に next フラグがある
- Api.TransformOptions に変換されている過程が不明
- Serverをalloc
- serverにBundleをアタッチ
- Bunlderの
configureLinker()
とconfigureRouter()
を呼び出し - この時にoptionsが使われるので、ここがアプリケーションと連携している部分になる
server.bundler = try allocator.create(Bundler);
server.bundler.* = try Bundler.init(allocator, &server.log, options, null, null);
server.bundler.configureLinker();
try server.bundler.configureRouter(true);
bundler.zig
-
configureLinker()
はLinkerフィールドを初期化する関数 -
configureRouter()
options.routesの情報を元にLinkerフィールドの振舞いを定義する関数 -
Bundler.init()
実行時にOptions.fromApi()
が呼び出されその引数にフレームワーク情報の設定が含まれている
bun-framework-next
- リポジトリに含まれるnpmパッケージ
- bunで作成したアプリケーションのdevDependenciesに追加されるのでこのモジュールがServerを仲介している
- そんなに特殊なことはしていなくてBun.serve でNextアプリケーションのリクエストをハンドリングするためのグルーコードになっている
- 実験としてbun bunでバイナリを作ってからこのモジュールを削除してみたところroutesが機能しなかったので実行時に読み込まれていることが分かる
node_modules.bun, node_modules.server.bun
❯ file node_modules.server.bun
node_modules.server.bun: sticky a /usr/bin/env bun script executable (binary data)
これらのバイナリって何なのということがイマイチ分かっていなかった。
最初は実行権限のついたバイナリだったのでバンドルした結果をネイティブコードにしているのかと思っていたけど中身を見たらそういうわけでもなかった。
#!/usr/bin/env bun
o<cd>
^@
var BUN_RUNTIME=(()=>{var fr=Object.create;var G=Object.defineProperty;var cr=Object.getOwnPropertyDescriptor;var sr=Object.getOwnPropertyNames;var ...
bun node_modules.bun
するとバイナリを上記のJSのコードとして取り出すことができる。
このファイルの中に node_modules/
以下のパスへの参照が入っていてなのでディレクトリを消すと動かなくなることが分かった。
Hot Module Replacement
next-dev-serverが持つHMRの仕組みをBunでどう実装しているのかというと、バンドル時にブラウザ上で動く以下のコードが実行されていた。
bundler/generate_node_modules_bundle.zig
fixed_buffer_writer.print(
\\if ('window' in globalThis) {{
\\ (function() {{
\\ BUN_RUNTIME.__injectFastRefresh(${x}());
\\ }})();
\\}}
__injectFastRefresh()
の実体はTSで実装された関数でreact-refresh で書かれている(ユーザーのアプリケーションのdevDependenciesに追加される)
runtime/hmr.ts
function injectFastRefresh(RefreshRuntime) {
if (!FastRefreshLoader.hasInjectedFastRefresh) {
RefreshRuntime.injectIntoGlobalHook(globalThis);
FastRefreshLoader.hasInjectedFastRefresh = true;
}
}
分ったこと
- Zigで書かれたHTTPサーバーが組込まれていてJSの依存モジュールを独自バイナリにバンドルして起動時に読み込むのでNodeで起動したサーバーがリクエストごとにJSを処理するのと比べて早くなるのではないか(予想)
- バンドル化やrefresh、リクエストのハンドリングなどのNext.jsのコアで行なわれていそうな処理もbunに含まれていた
- つまり
next dev
の高速版がbun dev
というインターフェイスを意図しているのだと思う
- つまり
- ただ
node_modules/
以下のJSはbun devの動作に必要なのでアプリケーションのすべてがバイナリになっているわけでもなかった- bun-framework-next自体はJSCのコンテキストで実行され、その内部からNext.jsのレンダリングコードを参照しているため
- 独自バイナリと
node_modules/
が連動している?
- Bun HTTPサーバーで動作するbun-framework-nextがNext.jsのインターフェイスでレスポンスを返す
- レンダリングにはNext.js本体のnpmパッケージのコードが使われる
- Bunの独自バイナリのバンドルと
node_modules/
が実行時に参照される - クライアントサイドのHot ReloadingはBunがバンドルにreact-refreshのJSコードを差し込む
Discussion