Rustで書かれたBiomeがNode.jsで動く仕組み
はじめに
Rust で自作したパッケージを Node.js のプロジェクトで動かしてみたくなりました。
同じく Rust で書かれた Biome ではどのように動かしているのだろう?と気になり調べてみました。
執筆時点(2025/03/15)での main ブランチのコードを読んでみたいと思います。
結論
今回参考にした Biome では、
「各 OS 向けにビルドしたバイナリを Node.js のchild_process.spawnSync()で実行」していました。
具体的には、
- 各 OS 向けにバイナリファイルのビルドを行う
- 各 OS 向けの
package.jsonを生成する -
npm install時に、実行環境にあったバイナリをダウンロードする -
npx biomeコマンドを実行した際に、child_process.spawnSync()経由でバイナリを実行する
というような流れになっています。
GitHub Actions でビルド
まず、GitHub Actions でバイナリファイルのビルドを行います。
strategy.matrixを使って、各 OS 向けの設定を変数化します。
上記で設定したmatrixの値を--target ${{ matrix.target }}のように指定して各 OS 向けのバイナリを生成します。
例えばtargetがaarch64-apple-darwinの場合、
cargo build -p biome_cli --release --target aarch64-apple-darwinが実行されます。
ビルドしたバイナリはtarget/${{ matrix.target }}に配置されます。
target/${{ matrix.target }}配下にはビルドしたバイナリ以外にも様々なファイルが生成されるため、
./dist/biome-${{ matrix.code-target }}のようにリネームし、./dist配下にバイナリファイルを集めます。
これを、actions/upload-artifactを使ってアップロードすることで、
後続のジョブでビルド済みのバイナリを参照できるようにします。
各 OS 向けのpackage.jsonを生成
続いて、npmに公開するため、各 OS 向けのpackage.jsonを生成します。
各 OS 向けのpackage.jsonは、GitHub Actions の中でpackages/@biomejs/biome/scripts/generate-packages.mjsを実行することで生成されます。
上記のスクリプトでは、まずベースとなるpackages/@biomejs/biome/package.jsonを読み込み、
各 OS 用のpackage.jsonを生成します。
ここで、manifestPathは@biomejs/<packageName>/package.jsonというパスになります。
例えば Apple Silicon の Mac 向けの場合、@biomejs/cli-darwin-arm64/package.jsonという値になります。
続いて、先ほどアップロードしたバイナリをactions/download-artifactでダウンロードし、
先ほどの@biomejs/<packageName>配下にコピーします。
その際に、実行権限の付与も行います。
これで、各 OS 向けのディレクトリ配下には、
package.jsonとバイナリが配置されました。
あとは各ディレクトリをループで回してnpmに公開すれば準備完了です。
optionalDependenciesで実行環境に合わせたバイナリをダウンロード
ここまでで、各 OS に向けたpackage.jsonとバイナリが配置されました。
ここからは、実際にnpm installした際の挙動を確認します。
biomeは、下記のコマンドを実行することでダウンロードすることができます。
npm i -D @biomejs/biome
上記のコマンドを実行すると、node_modules配下の@biomejs/biomeは下記のような形になります。
node_modules
├── .bin
│ └── biome -> ../@biomejs/biome/bin/biome
├── .package-lock.json
└── @biomejs
├── biome
│ ├── LICENSE-APACHE
│ ├── LICENSE-MIT
│ ├── README.hi.md
│ ├── README.ja.md
│ ├── README.kr.md
│ ├── README.md
│ ├── README.pt-BR.md
│ ├── README.zh-CN.md
│ ├── README.zh-TW.md
│ ├── ROME-LICENSE-MIT
│ ├── bin
│ │ └── biome
│ ├── configuration_schema.json
│ ├── package.json
│ └── scripts
│ └── postinstall.js
└── cli-darwin-arm64
├── biome
└── package.json
今回私は Apple Silicon の Mac で実行したため、
biomeとcli-darwin-arm64というディレクトリが作成されています。
(.binについては後述します。)
npm i -D @biomejs/biomeを実行した際、
まず下記のパッケージが参照されます。
続いて、上記のパッケージのpackage.json(= @biomejs/biome/package.json)の
optionalDependenciesに登録されている値(= 各 OS 向けに生成したpackage.json)を確認します。
package.jsonのosやcpuの項目を確認し、
実行環境と一致するパッケージのみが追加でダウンロードされ、
それ以外のパッケージは無視されます。
(この時、実行環境に一致しないパッケージがあってもエラーにならないようにoptionalDependenciesになっているのだと思います)
npx biomeコマンドを実行
最後に、
npx biome
した際の挙動を確認します。
ダウンロードする際は、@biomejs/biomeという名前でダウンロードしたのに、
npx biome xxxとして実行できるのはなぜでしょうか?
先ほどのnode_modules配下を改めて見ると、
下記の通り.bin/biomeが@biomejs/biome/bin/biomeへのシンボリックリンクになっていることが確認できます。
node_modules
├── .bin
│ └── biome -> ../@biomejs/biome/bin/biome
├── .package-lock.json
└── @biomejs
├── biome
│ ├── LICENSE-APACHE
│ ├── LICENSE-MIT
│ ├── README.hi.md
│ ├── README.ja.md
│ ├── README.kr.md
│ ├── README.md
│ ├── README.pt-BR.md
│ ├── README.zh-CN.md
│ ├── README.zh-TW.md
│ ├── ROME-LICENSE-MIT
│ ├── bin
│ │ └── biome
│ ├── configuration_schema.json
│ ├── package.json
│ └── scripts
│ └── postinstall.js
└── cli-darwin-arm64
├── biome
└── package.json
これは、package.jsonのbinに記載されているキー名が、
.bin配下の名前として登録できるためです。
実際にpackage.jsonを確認すると、"biome": "bin/biome"という値で登録されていることがわかります。
さらに、bin/biomeのファイルを確認すると、下記のようになっています。
簡単に処理内容を説明すると、
- Node.js の
process.platformやprocess.archの値から、バイナリのパスを判定する - 判定したパスのバイナリを
child_process.spawnSync()で実行する
という処理を行なっています。
まとめると、
-
npx biomeを実行する -
node_modules/.bin/biomeのシンボリックリンクを参照する - シンボリックリンクの参照先の
@biomejs/biome/bin/biomeが実行される -
@biomejs/biome/bin/biomeの中でバイナリをパスを判定する - 判定したバイナリを
child_process.spawnSync()で実行する
という流れになっています。
おわりに
今回は Rust で書かれた Biome が、Node.js のプロジェクトでどのように実行されているかを調べてみました。
気になっていたことが解消されただけでなく、
GitHub Actions の書き方や自動でリリースノートを作成する方法など、
学びになる部分がたくさんありました。
今回 Cursor ちゃんと一緒にコードリーディングを行いましたが、
改めて LLM の便利さを実感しました。
コードリーディングへのハードルがかなり下がったため、
今後も機会があれば、気になったリポジトリを読んでみようと思います。
Discussion