👻

HonoでReact SPA最小をBunに対応させる

2024/02/29に公開

Iaiso さんから端を発して Hono 作者の yusukebe さんが記事を書いていたので、そのリポジトリを元にして Bun.sh 対応を試みました

https://github.com/yoshikouki/hono-spa-react-bun

この記事の発端となったお二人の記事

https://zenn.dev/yusukebe/articles/06d9cc1714bfb7
https://zenn.dev/laiso/articles/c7eba95ce43feb

これらの記事は Cloudflare Pages へのデプロイが前提となっていますが、今回はどのデプロイ先でも行けるような構成にしてみました

始め方

始め方は yusukebe さんの記事「HonoでAPI付き雑React SPA最小」 の記事が Bun をパッケージマネージャーとして使っているため、ほぼ流用できます。そのためここでは割愛します

Bun 上で実行するための差分

以下のコミットを参照してもらえると大枠把握できると思います

https://github.com/yoshikouki/hono-spa-react-bun/commit/177ac9f4550ae00f81a10ac2ee8f2e86f50389ba

package.json

package.json
  "scripts": {
-   "dev": "vite",
-   "build": "vite build --mode client && vite build",
-   "preview": "wrangler pages dev dist",
-   "deploy": "$npm_execpath run build && wrangler pages deploy dist"
+   "dev": "bunx vite",
+   "build": "bun run build:client #TODO: && bun run build:server",
+   "build:client": "bun --bun x vite build --mode client",
+   "build:server": "bun --bun x vite build",
+   "serve": "NODE_ENV=production bun --bun run bin/serve.ts"
  .
  .
  .
  "devDependencies": {
-   "@cloudflare/workers-types": "^4.20240208.0",
-   "@hono/vite-cloudflare-pages": "^0.2.4",
    "@hono/vite-dev-server": "^0.8.0",
+   "@types/bun": "^1.0.8",
    "@types/react": "^18.2.60",
    "@types/react-dom": "^18.2.19",
    "vite": "^5.0.12",
-   "wrangler": "^3.25.0"

ビルドはクライアントだけで、サーバーサイドは Bun でそのまま実行する形にしています。Bun は TypeScript ファイルを直接実行でき、そのオーバーヘッドはわずかであると公式ドキュメントには記されています。

Is transpiling still necessary? — Because Bun can directly execute TypeScript, you may not need to transpile your TypeScript to run in production. Bun internally transpiles every file it executes (both .js and .ts), so the additional overhead of directly executing your .ts/.tsx source files is negligible.

https://bun.sh/docs/runtime/typescript#running-ts-files

そのため、今回は TypeScript ファイルを直接実行する形を取りました

bun dev--bun オプションを付けていないのは、現在まだ対応していないためです

Yeah. It does not support running on Bun. It may run on Bun in the future, but for now, please run Vite on Node.js.

https://github.com/honojs/honox/issues/91#issuecomment-1962824857

vite.config.ts

(多いので一部の差分のみ。全ての差分はコミットを参照してください)

vite.config.ts
-import pages from '@hono/vite-cloudflare-pages'
.
.
export default defineConfig(({ mode }) => {
  if (mode === 'client') {
  .
  .
  } else {
    return {
    .
    .
      plugins: [
-       pages(),
        devServer({
-         entry: 'src/index.tsx'
+         entry: 'src/server.tsx'
        })
      ]

主な変更点は、Hono 公式プラグインの @hono/vite-cloudflare-pages による設定を展開しつつも、Cloudflare 文脈のものを変更しました

https://github.com/honojs/vite-plugins/tree/main/packages/cloudflare-pages

本当のところを言うと、Bun で TypeScript を直接実行する今回の要件では、Vite は開発サーバー devServer の設定と mode === 'client' の設定だけで十分です
(そのため、差分は page() の削除だけで良い)

ただ個人的な今後の課題として、Vite でサーバーもビルドしたい欲があるため、今回は設定を盛り込んだままにしました(つまり未完成の差分)

src/index.tsx

src/index.tsx
-app.get('*', (c) => {
+app.get('/', (c) => {
+  const isProduction =
+    process.env.NODE_ENV === "production" || import.meta.env.PROD;
     .
     .
     .
-         {import.meta.env.PROD ? (
+         {isProduction ? (
            <script type="module" src="/static/client.js"></script>
          ) : (
            <script type="module" src="/src/client.tsx"></script>
          )}

本番環境のアセット配信のため、ルーティングを * からルートパス / へ変更しました。

今回の実装ではサーバー側が vite build を経由していないため、import.meta.env.PROD などを例とする Vite の恩恵を享受できません。そのため、src/server.ts (yusukebe さんによる元リポジトリでは src/index.tsx) の当該部分を、process.env.NODE_ENV === "production" || import.meta.env.PROD と緩める形を取りました

bin/serve.ts

bin/serve.ts
import { serve } from "bun";
import { serveStatic } from "hono/bun";

import app from "../src/server";

const port = process.env.PORT || 3000;

app.use("/static/client.js", serveStatic({ root: "dist" }));
app.use("/static/*", serveStatic({ root: "public" }));

console.log(`Listening on http://localhost:${port}`);

serve({
    port,
    fetch: app.fetch
})

本番環境用の起動スクリプトです。開発環境はサーバーも Vite で動かしたいため src/server.tsxexport default app (Hono インスタンス) するようにして、本番環境では別途スクリプトを用意するようにしました

ご覧の通り、アセット配信の設定が開発環境と若干異なるため、その差分を吸収する役割も担っています

まとめ

ファイル構造はまだシンプルですね!

.
├── README.md
├── bin
│   └── serve.ts
├── package.json
├── public
│   └── static
│       └── style.css
├── src
│   ├── client.tsx
│   └── server.tsx
├── tsconfig.json
└── vite.config.ts

ということで Hono の React SPA を Bun で動かすための構成でした!
Hono は Bun との相性が良いように感じているので、引き続き気付いた点があったらまとめていきます💪

Discussion