BunとHonoでActivityPub実装を作ったので紹介とかします。Denoとか
Intro
その名もMatchbox。
Bunを使いました。
Hono[炎]っていうイケてる名前のフレームワークを使って作りました。
どこかで見たことある流れですね。
そうです。以前Denoで作ったものをフォークして少しコードを変えただけです。
別にDenoからBunに切り替えたわけではなく、MatchboxはDenoのままです。
Bun 0.2.2でWeb Crypto APIが実装されたと聞いたので、Hono使っているし多分動かせるだろうと思いやってみました。
Mastodonでもちゃんと動くことを確認済みです。
ではどのようにマイグレーションすればBunで動かせるのか紹介したいと思います。
- Matchbox 0.3.0
- Bun 0.2.2
- Deno 1.27.0
- Node.js 18.12.0
- Hono 2.3.2
- TypeScript 4.8.3, TypeScript 4.8.4
Hono
まずはMatchbox 0.3.0のソースコードをダウンロードするなりGitからクローンするなりGitpodボタンクリックするなりして用意します。
--- deno-index.ts 2022-10-30 09:50:00.000000000 +0000
+++ bun-index.ts 2022-10-30 09:50:00.000000000 +0000
@@ -1,17 +1,13 @@
-import "dotenv";
-import { serve } from "server";
import { Hono } from "hono";
import { basicAuth } from "hono/basic-auth";
-import { serveStatic } from "hono/serve-static";
+import { serveStatic } from "hono/serve-static.bun";
const ENV = {
- HOST: Deno.env.get("HOST"),
- PORT: Deno.env.get("PORT"),
- ENABLE_BASIC_AUTH: Deno.env.get("ENABLE_BASIC_AUTH"),
- BASIC_AUTH_USERNAME: Deno.env.get("BASIC_AUTH_USERNAME"),
- BASIC_AUTH_PASSWORD: Deno.env.get("BASIC_AUTH_PASSWORD"),
- SECRET: Deno.env.get("SECRET"),
- PRIVATE_KEY: Deno.env.get("PRIVATE_KEY"),
+ PORT: process.env.PORT,
+ ENABLE_BASIC_AUTH: process.env.ENABLE_BASIC_AUTH,
+ BASIC_AUTH_USERNAME: process.env.BASIC_AUTH_USERNAME,
+ BASIC_AUTH_PASSWORD: process.env.BASIC_AUTH_PASSWORD,
+ SECRET: process.env.SECRET,
+ PRIVATE_KEY: process.env.PRIVATE_KEY,
} as { [key: string]: string };
const app = new Hono();
@@ -616,7 +612,7 @@
return c.redirect(`/u/${c.req.param("strRoot").slice(1)}`);
});
-serve(app.fetch, {
- hostname: ENV.HOST || "localhost",
+export default {
port: Number(ENV.PORT) || 8000,
-});
+ fetch: app.fetch,
+};
こう修正すれば動きます。
実際に変えた部分を解説していきたいと思います。
ESM
import { Hono } from "hono";
import { basicAuth } from "hono/basic-auth";
import { serveStatic } from "hono/serve-static.bun";
Denoではdotenv, serveモジュールをESMとして読み込んでいましたがBunではランタイムに組み込まれているのでインポートする必要ありません。削りましょう。
またHonoのserve-staticをインポートする際はBun専用のミドルウェアとして読み込む必要があるので"hono/serve-static.bun"と書くことに注意が必要です。
ちなみにMatchbox自体BunやNode.jsと互換性を取れるようDenoのimport_map.jsonを使うよう改良しました。
env
const ENV = {
PORT: process.env.PORT,
ENABLE_BASIC_AUTH: process.env.ENABLE_BASIC_AUTH,
BASIC_AUTH_USERNAME: process.env.BASIC_AUTH_USERNAME,
BASIC_AUTH_PASSWORD: process.env.BASIC_AUTH_PASSWORD,
SECRET: process.env.SECRET,
PRIVATE_KEY: process.env.PRIVATE_KEY,
} as { [key: string]: string };
DenoではDeno.env.get("PORT")
を使い環境変数を読み込んでいましたがBunはNode.jsと同様にprocess.env.PORT
と書いて読み込みます。
ちなみにBun.env.PORT
と書いても大丈夫です。
地味にこれが結構難しくて、環境変数はあらかじめ定数として定義しておかないとランタイムごとに一つ一つ該当箇所を書き換えないといけない問題が発生します。
Honoでは環境変数を読み込むAPIのc.env.PORT
を用意していますがそれはcontextオブジェクトを使用できるハンドラ内に限った話なので、グローバル変数として使いたいときはこのようにまとめて最初に定義しておくことで複数のランタイム対応が楽になるため今回そのようにしました。
なお、この環境変数API問題はWinterCGでも議論されています。
Serve
export default {
port: Number(ENV.PORT) || 8000,
fetch: app.fetch,
};
先程のenvでは説明を飛ばしましたが環境変数HOSTを削りました。
というのも現時点でBunのServeはhostnameの引数が用意されていないのです。
Node.jsやDenoをはじめとしたHTTPサーバーではhostnameの引数が用意されているため意外でした。
こちらもDenoでserve()
やUnstableなDeno.serve()
が用意されているように、BunではBun.serve()
が用意されています。
またBun.serve()
はCloudflare Workersのようにfetchを含むオブジェクトをexport defaultとして置き換えることもできるので今回そのように書きました。
……たったこれだけソースコードを修正するだけで動くんですね。恐るべしBun。恐るべしHono。
package.json
このようにBunはDeno以上にNode.jsとの互換性を非常に重視していることがわかります。
なのでnpmで使われているpackage.jsonもNode.jsの部分をBunに書き換えるだけでパッケージのインストールやランタイムの起動が楽になります。
{
"name": "matchbox",
"version": "0.3.0",
"description": "Matchbox",
"main": "index.ts",
"scripts": {
"start": "bun -u $(gp url 8000) index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://gitlab.com/acefed/matchbox.git"
},
"keywords": [
"hono"
],
"author": "Acefed",
"license": "MIT",
"bugs": {
"url": "https://gitlab.com/acefed/matchbox"
},
"homepage": "https://gitlab.com/acefed/matchbox",
"module": "index.ts",
"type": "module",
"dependencies": {
"hono": "^2.3.2"
},
"devDependencies": {
"@types/node": "^18.11.7"
}
}
このように書いたpackage.jsonを追加しておきます。
CLI
さて、BunはNode.jsやDenoに似ているものの、少し異なるコマンドが非常に多いため、CLIを使うときは注意が必要です。
$ curl https://bun.sh/install | bash
$ export BUN_INSTALL="$HOME/.bun"
$ export PATH="$BUN_INSTALL/bin:$PATH"
まずはBunをインストールします。
Denoと非常に似ていますね。
$ bun i --backend copyfile
bun install
を行うことで、package.jsonに記載されているパッケージをインストールできます。
この際環境によってはデフォルトのhardlinkやsymlinkではパッケージを読み込みない場合があるので、その際はcopyfileを--backend
オプションで指定します。
$ bun start
あとはbun run index.ts
をするだけです。npmのようにpackage.jsonにstartスクリプトを用意しておけばnpm run start
と書くことができ、更に短いbun start
と書くこともできます。何ならbun run
はbun
に省略できるのでbun index.ts
と書いても動きます。
ここで注意したいのが、このまま動かしてもソースコード内のURLがhttp://0.0.0.0:8000/
みたいな感じになり、localhostやドメインの割当ができず動作に不具合が発生する可能性があることです。
そこで--origin
オプションが必要になります。--origin
は-u
に省略できます。
$ bun -u "$(gp url 8000)" index.ts
今回はGitpodで動かしたのでこのような書き方になりましたが、-u http://localhost:8080
や-u http://example.com:80
といった感じでドメインの書き換えを行うようにしましょう。
TypeScript
Bunは拡張子が.tsでも問題なく読み込むことができますが、Denoのように型チェックなど行える機能はありません。
必要に応じてTypeScriptをインストールする必要があります。
$ bun a -d typescript
$ bun i --backend copyfile
まずはbun add
を使いpackage.jsonにTypeScriptを追加します。
再度インストールすればtscコマンドが使えるようになります。
$ node_modules/.bin/tsc -v
/usr/bin/env: ‘node’: No such file or directory
いや使えるようにならんわ。
記事公開してから気づいたけどNode.jsに依存してた……。
こうして無理やり使えるようにします。
$ tar -xJf node-v18.12.0-linux-x64.tar.xz --strip-components=2 node-v18.12.0-linux-x64/bin/node
$ rm node-v18.12.0-linux-x64.tar.xz
$ ./node node_modules/.bin/tsc -v
Version 4.8.4
よかったですね。
$ ./node node_modules/.bin/tsc index.ts -t es2020 -m node16 --skipLibCheck
あとはこんな感じで書けば型チェックを行いコンパイルされたindex.jsが生成されます。
$ bun -u "$(gp url 8000)" index.js
もちろんindex.tsファイルと同様にindex.jsファイルも動かせます。
Experimental
今回紹介したBun版Matchboxはソースコード公開していませんがDiff公開してみたので多分こんな感じで順番にコマンド入力すれば動きます。
$ git clone https://gitlab.com/acefed/matchbox.git
$ cd matchbox
$ git checkout 9f9e7854
$ curl https://acefed.gitlab.io/matchbox/9f9e7854.diff | git apply
$ bash env.sh
$ curl https://bun.sh/install | bash
$ export BUN_INSTALL="$HOME/.bun"
$ export PATH="$BUN_INSTALL/bin:$PATH"
$ bun i --backend copyfile
$ bun start
実験的なやつですのでそのうち動かなくなるかもしれないです。
Outro
もうHonoとActivityPubの解説いいですよね。
何にせよBunとHonoの互換性に驚くばかりでです。
またActivityPubはWeb APIと非常に相性が良いことも分かります。
まあ現在のMatchboxにはファイルシステムやSQLiteに関する機能を実装していないので、そこら辺が絡むと一気にマイグレーションが難しくなるだろうなと予測しています。
今回は一旦筆を置き、Node.jsとDenoとBunの何が同じで何が異なるのか今後注力していきたいと思います。
Ref
Discussion