bun-hot-reloading について
Bun の `--watch` と `--hot` を使う開発向けスキルです。プロセス再起動とモジュールのインプレース再読み込みを選び、HTTP サーバーやテスト、状態管理に適用できます。
主な機能
- 01`--watch` と `--hot` の動作比較
- 02ファイル変更時のプロセス再起動
- 03状態を保つモジュール再読み込み
- 04`import.meta.hot` コールバックの利用
- 05ウォッチモードでのテスト実行
活用シーン
- 01開発中の Bun アプリ再起動
- 02状態を失わない Bun HTTP サーバーの再読み込み
- 03ソース変更後のテスト再実行
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ secondsky/ claude-skills --skill bun-hot-reloadingインストール後、Claude Code で/bun-hot-reloadingを入力して読み込まれたか確認します。
詳細
Bun のホットリロード
Bun には、開発サイクルを高速化するホットリロード機能が組み込まれています。
Watch モードと Hot モード
| 機能 | --watch | --hot |
|---|---|---|
| 動作 | プロセスを再起動 | モジュールを再読み込み |
| 状態 | 再読み込み時に失われる | 保持される |
| 速度 | 約 20ms で再起動 | 即座に再読み込み |
| 用途 | あらゆるファイル形式 | Bun.serve HTTP |
Watch モード(--watch)
ファイルが変更されると、プロセス全体を再起動します。
# Basic watch mode
bun --watch run src/index.ts
# Watch specific script
bun --watch run dev
# Watch with test runner
bun --watch test
package.json のスクリプト
{
"scripts": {
"dev": "bun --watch run src/index.ts",
"dev:server": "bun --watch run src/server.ts",
"test:watch": "bun --watch test"
}
}
Watch の動作
- インポートされたファイルを自動的に監視します
.ts、.tsx、.js、.jsxの変更を検知します.jsonのインポートも監視します- 状態を初期化して再起動します
Hot モード(--hot)
プロセスを再起動せずに、モジュールをその場で再読み込みします。
bun --hot run src/server.ts
HTTP サーバーのホットリロード
// src/server.ts
let counter = 0; // State preserved across hot reloads
export default {
port: 3000,
fetch(req: Request) {
counter++;
return new Response(`Request #${counter}`);
},
};
bun --hot run src/server.ts
server.ts を変更すると、counter の値を保持したまま、モジュールが即座に再読み込みされます。
ホットリロードを使用した Bun.serve
// src/server.ts
const server = Bun.serve({
port: 3000,
fetch(req) {
return new Response("Hello!");
},
});
// Hot reload handler
if (import.meta.hot) {
import.meta.hot.accept(() => {
console.log("Hot reload!");
});
}
console.log(`Server running on port ${server.port}`);
import.meta.hot API
// Check if hot reload is available
if (import.meta.hot) {
// Accept updates to this module
import.meta.hot.accept();
// Accept with callback
import.meta.hot.accept((newModule) => {
console.log("Module updated:", newModule);
});
// Cleanup before reload
import.meta.hot.dispose(() => {
// Close connections, clear intervals, etc.
clearInterval(myInterval);
});
// Decline hot reload (force full restart)
import.meta.hot.decline();
// Invalidate this module (trigger parent reload)
import.meta.hot.invalidate();
}
HTTP サーバーのパターン
Express のようなパターン
// src/server.ts
import { createApp } from "./app";
const app = createApp();
const server = Bun.serve({
port: 3000,
fetch: app.fetch,
});
// Hot reload: recreate app
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
// Reload with new fetch handler
server.reload({
fetch: newModule.default.fetch,
});
});
}
状態を持つサーバー
// src/server.ts
// Store in globalThis to survive reloads
globalThis.connections ??= new Set();
const server = Bun.serve({
port: 3000,
fetch(req) {
return new Response(`Connections: ${globalThis.connections.size}`);
},
websocket: {
open(ws) {
globalThis.connections.add(ws);
},
close(ws) {
globalThis.connections.delete(ws);
},
},
});
if (import.meta.hot) {
import.meta.hot.accept();
}
カスタム Watch の実装
// dev-server.ts
import { watch } from "fs";
const srcDir = "./src";
let server: ReturnType<typeof Bun.serve> | null = null;
async function startServer() {
// Dynamic import with cache busting
const module = await import(`./src/server.ts?t=${Date.now()}`);
if (server) {
// Bun 1.2+: server.stop() returns a Promise — await it so the old
// socket is fully torn down before we rebind the same port.
await server.stop();
}
server = Bun.serve(module.default);
console.log(`Server started on port ${server.port}`);
}
// Initial start
await startServer();
// Watch for changes
watch(srcDir, { recursive: true }, async (event, filename) => {
if (filename?.endsWith(".ts") || filename?.endsWith(".tsx")) {
console.log(`\n[${event}] ${filename}`);
await startServer();
}
});
console.log("Watching for changes...");
WebSocket のライブリロード
サーバー
// src/dev-server.ts
const clients = new Set<ServerWebSocket>();
const server = Bun.serve({
port: 3000,
fetch(req, server) {
if (req.headers.get("upgrade") === "websocket") {
server.upgrade(req);
return;
}
// Inject reload script in dev
const html = `
<!DOCTYPE html>
<html>
<body>
<h1>Hello!</h1>
<script>
const ws = new WebSocket('ws://localhost:3000');
ws.onmessage = (e) => {
if (e.data === 'reload') location.reload();
};
</script>
</body>
</html>
`;
return new Response(html, {
headers: { "Content-Type": "text/html" },
});
},
websocket: {
open(ws) {
clients.add(ws);
},
close(ws) {
clients.delete(ws);
},
},
});
// Notify clients on file change
watch("./src", { recursive: true }, () => {
clients.forEach((ws) => ws.send("reload"));
});
Vite との統合
HMR を使用したフロントエンド開発の場合:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
hmr: true,
},
});
# Use Bun to run Vite
bunx --bun vite
Watch モードでのテスト
# Watch tests
bun --watch test
# Watch specific file
bun --watch test src/utils.test.ts
# With bail (stop on first failure)
bun --watch test --bail
環境の検出
// Check if running with --hot
const isHot = !!import.meta.hot;
// Check if running with --watch
const isWatch = process.env.BUN_WATCH === "1";
// Development mode
const isDev = process.env.NODE_ENV !== "production";
if (isDev) {
console.log("Running in development mode");
console.log(`Hot reload: ${isHot}`);
console.log(`Watch mode: ${isWatch}`);
}
よくある問題
状態が保持されない
// ❌ State lost on hot reload
let cache = new Map();
// ✅ State preserved on hot reload
globalThis.cache ??= new Map();
const cache = globalThis.cache;
クリーンアップが実行されない
// ❌ Interval keeps running after reload
setInterval(() => console.log("tick"), 1000);
// ✅ Clean up on dispose
const interval = setInterval(() => console.log("tick"), 1000);
if (import.meta.hot) {
import.meta.hot.dispose(() => {
clearInterval(interval);
});
}
モジュールが再読み込みされない
// ❌ Import not watched
const config = require("./config.json");
// ✅ Use import for watching
import config from "./config.json";
よくあるエラー
| エラー | 原因 | 解決方法 |
|---|---|---|
Changes not detected | ファイルがインポートされていない | インポートの連鎖を確認します |
State lost | --watch を使用している | --hot または globalThis を使用します |
Port in use | サーバーが停止していない | 再バインドする前に await server.stop() を実行します |
Memory leak | クリーンアップがない | dispose コールバックを使用します |
参照資料を読み込むタイミング
次の場合は references/advanced-hmr.md を読み込みます:
- カスタム HMR プロトコル
- モジュールフェデレーション
- 複雑な状態管理
次の場合は references/debugging.md を読み込みます:
- HMR が動作しない
- 状態に関する問題
- パフォーマンスのデバッグ
コメント
Markdown 対応