🙄

bun devでNext.jsが実行される仕組みを調べる

2022/07/11に公開

https://github.com/oven-sh/bun

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()
https://github.com/oven-sh/bun/blob/c3bf97002d6f0b117060dabf2aa281eedd85b7fd/src/http.zig#L3941

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/が連動している?
  1. Bun HTTPサーバーで動作するbun-framework-nextがNext.jsのインターフェイスでレスポンスを返す
  2. レンダリングにはNext.js本体のnpmパッケージのコードが使われる
  3. Bunの独自バイナリのバンドルとnode_modules/が実行時に参照される
  4. クライアントサイドのHot ReloadingはBunがバンドルにreact-refreshのJSコードを差し込む

Discussion