---
title: "デスクトップアプリ（Electrobun）設計書"
---

最終更新: 2026-08-27

## 1. 概要

MorningStatusApp デスクトップアプリ（Windows／macOS）のアーキテクチャ設計書。Next.js standalone サーバーを Electrobun でラップし、単一のネイティブアプリとして配布する仕組みを扱う。

この領域のドキュメントはこれまで `docs/notes/DEVELOPMENT_NOTES.md` の断片的な知見や README の変更履歴に散在しており、設計の全体像をまとめた文書が存在しなかった。Windows/macOS でディレクトリ構成が異なる点（§3）のような前提知識が実装者間で共有されず、バグ（#1535）を誘発したことを受けて本書を新設した（#73）。

ビルド・インストールの実行手順（コマンド）は [デスクトップアプリのビルド手順](/operations/desktop-build) に分離しており、本書はアーキテクチャ・実装仕様（Why・How が本質的に変わらない部分）に特化する。

---

## 2. ビルドフロー概要

1. `DESKTOP_MODE=1 bun run next build` で Next.js standalone サーバー（`.next/standalone/`）を生成
2. `bunx electrobun build` で Electrobun アプリ（プラットフォーム別バイナリ）を生成
3. 1 の standalone サーバー一式を、2 の成果物（`.app` バンドル／インストーラ用ディレクトリ）の `Resources` 配下にコピー

具体的なコマンド・スクリプト（`setup/build-macos.sh`・`setup/build-installer.ps1`）は [デスクトップアプリのビルド手順](/operations/desktop-build) を参照。

---

## 3. プラットフォーム別ディレクトリ構成の違い

インストール済みアプリのディレクトリ構成は、Windows と macOS で「実行バイナリの配置ディレクトリ名」が異なる。両者とも実行バイナリ配置ディレクトリと `Resources` が同階層にあるという点は共通しているが、ディレクトリ名（`bin` か `MacOS` か）が異なるため、パス解決ロジックはプラットフォームごとに個別の検証が必要になる。

| 要素 | Windows（Inno Setup、フラット構成） | macOS（`.app` バンドル仕様） |
| --- | --- | --- |
| 同梱 bun 実行ファイル | `{app}/bin/bun.exe` | `{app}.app/Contents/MacOS/bun` |
| ランチャー本体 | `{app}/launcher.exe` | `{app}.app/Contents/MacOS/launcher` |
| Electrobun ワーカー（`index.js`） | `{app}/Resources/app/bun/index.js` | `{app}.app/Contents/Resources/app/bun/index.js` |
| Next.js standalone サーバー | `{app}/Resources/nextjs/standalone/` | `{app}.app/Contents/Resources/nextjs/standalone/` |

実際のパス解決は `desktop/src/bun/resolve-paths.ts` の `resolveBunExecutablePath(resourcesDir, platform)` に集約している。`index.js` の配置ディレクトリ（`Resources/app/bun` 相当）を起点に、`darwin` なら `../MacOS/bun`、それ以外（Windows）なら `../bin/bun.exe` を返す。

> **備考:** 旧実装（#1535）は、macOS でも Windows と同じ計算式（`{resourcesDir}/../bin/bun` 相当）で bun 実行ファイルのパスを求めていた。macOS 対応自体は #711 で追加されたが、インストール済み `.app` での実機起動検証が行われておらず、実在する `Contents/MacOS/bun` を参照できずに `ENOENT` で起動失敗していた。プラットフォーム別のパス解決ロジックを `resolve-paths.ts` に切り出し、`darwin` の場合のみ `../MacOS/bun` を返すように修正して解消した。

---

## 4. 起動フロー（インストール済みアプリ）

1. `launcher.exe`（Windows）／`MorningStatusApp`（macOS）→ Electrobun ランチャーが起動し、`bun/index.js`（Worker）を起動する
2. Worker が `resolveBunExecutablePath()` で同梱 bun 実行ファイルのパスを解決する（プラットフォーム別、§3参照）
3. 前回起動時の自プロセス（`main.js` = このWorker自身）のPID（OS一時ディレクトリの `morning-status-app-desktop-main.pid` に記録、`MAIN_LOCK_FILE`）が生存していれば `killStaleServer()` で強制終了し、ロックファイルを削除したうえで自身のPIDを書き込む。`server.js` の有無に関わらず常に実行する（#1563）
4. 前回起動時のサーバー PID（OS 一時ディレクトリの `morning-status-app-desktop-server.pid` に記録、`SERVER_LOCK_FILE`）が生存していれば `killStaleServer()` で強制終了し、ロックファイルを削除する（#1561）
5. `Resources/nextjs/standalone/server.js` を検出し、同梱 bun で Next.js サーバーを子プロセス起動、そのプロセス PID をロックファイルに書き込む（`writeLockedPid()`）
6. `waitForServer()` がサーバーのポート応答をポーリングする。応答前にサーバープロセスが終了した場合は起動失敗として扱う
7. `BrowserWindow` で `http://localhost:{port}`（既定 `3001`）を表示する
8. WebView2 は初回描画時にスクロールバーを描画しないため、`dom-ready` イベント後にウィンドウサイズを ±1px 変化させて再描画を強制する

> **備考:** 以前は上記6の待機処理がポートへの応答有無だけを起動成功の判定基準にしており、ウィンドウを閉じても `process.on("exit")` のクリーンアップを素通りしてサーバー子プロセスが残留することがあった。再ビルド・再インストール後に起動すると、新しいサーバーがポートの bind に失敗し、残留していた旧バージョンのサーバーにそのまま接続してしまい、再ビルドしても画面が更新されないバグがあった（#1561）。PID ロックファイルによる残留プロセスの検出・強制終了（上記4・5）と、起動確認時のプロセス終了検知（上記6）を追加して解消した。

---

## 5. プロセス終了時のクリーンアップ

- **正常終了**: `process.on("exit")` および `app.on("before-quit", ...)`（Electrobun の `app` export）で、spawn した Next.js サーバー子プロセスを `kill()` する
- **異常終了（強制終了・クラッシュ等）への対策**: 上記フックは異常終了時には発火しないため、サーバー子プロセス（`server.js`）は §4-4 のロックファイル方式で次回起動時に自己修復する（#1561）。`main.js` 自身（Electrobun ランタイム本体）についても、`launcher.exe` の異常終了時には上記フックが発火しないため、同じロックファイル方式（§4-3、`MAIN_LOCK_FILE`）で次回起動時に自己修復する（#1563）

> **備考:** 以前は `main.js` 自身を保護する仕組みが無く、`launcher.exe`（Electrobun ランタイム本体、`main.js` を実行する `bun.exe` の親プロセス）が異常終了すると `main.js` が孤児プロセスとして残留していた。ウィンドウ（WebView2）は `launcher.exe` ではなく `main.js` 側が保持しているため、残留した `main.js` は操作可能な旧ウィンドウごと生存し続けていた。この状態でインストーラの上書きアップグレードを実行すると、ファイルにロックがかかり失敗する場合があった（#1563）。`server.js` と同じ自己ロックファイル方式を `main.js` 自身にも適用し解消した。

---

## 6. WebView のキャッシュ制御

デスクトップモードの Next.js レスポンスは `next.config.ts` の `headers()` で `Cache-Control: no-store` を付与し、Next.js の ISR が内部的に設定する `Cache-Control`（`s-maxage`／`stale-while-revalidate` 等）を上書きする。適用条件は以下の2点で、Web 版（Vercel）の ISR キャッシュ設計には影響させない。

- **`DESKTOP_MODE=1` かつ macOS 限定**: WKWebView（macOS）でのディスクキャッシュ不具合は実機調査で再現・検証済みだが、WebView2（Windows）では同種の不具合が未確認。Windows は macOS よりインストール・アンインストールの検証回数が大幅に多いにもかかわらず再現報告がないため、根拠のないまま Windows にも同じ対策を適用するのは妥当でないと判断し、`process.platform` で macOS 限定にガードしている
- **`_next/static` 配下は対象外**: `source: '/((?!_next/static).*)'` により除外する。同ディレクトリはコンテンツハッシュ付きファイル名で無期限キャッシュしてよいため、no-store の対象に含める必要がない

> **備考:** 以前は Web 版と同じ ISR キャッシュヘッダーをそのまま返しており、WKWebView（macOS）が `stale-while-revalidate` に従って HTML をディスクキャッシュしていた。このキャッシュは `.app` の再インストールでは消えないため（Electrobun の `BrowserView` にキャッシュを明示的にクリアする API が無い）、サーバー自体が新バージョンで起動できていても画面には反映されないケースがあった（#1561）。デスクトップモードはローカル単一ユーザー向けで CDN キャッシュの恩恵がなく、常に最新の内容を表示すべきであるため、`no-store` を強制することで解消した。当初は全ルート・全プラットフォームに適用していたが、`_next/static` 配下の静的アセットまで無効化対象に巻き込んでいた点と、WebView2（Windows）では不具合が未確認のまま対策を適用していた点を別PRのレビュー（#1562）で指摘され、上記の適用条件に絞り込んだ。

---

## 7. 同梱 Bun バイナリのバージョン管理

`electrobun` パッケージは、システムの `bun upgrade` とは独立して、自前で固定バージョンの Bun バイナリを同梱する仕組みを持つ。バージョンは `desktop/electrobun.config.ts` の `build.bunVersion`（現在: `1.4.0`）で明示指定しており、上げる場合はこのフィールドを更新する必要がある。

---

## 8. バージョン管理（アプリ本体）

アプリのバージョンは目的の異なる3箇所で個別に管理されており、自動同期される仕組みはない。

| 管理箇所 | 用途 | 現在値（例） |
| --- | --- | --- |
| root `package.json` の `version` | Next.js アプリが画面に表示するバージョン（フッター「データ最終更新」欄等）。Windows インストーラのバージョンも `build-installer.ps1` が本値を読み取り `/DAppVersion` として Inno Setup に渡す | `9.24.1` |
| `desktop/electrobun.config.ts` の `app.version` | Electrobun アプリ本体（`.app`／DMG）のバージョン | `0.0.4` |
| `desktop/package.json` の `version` | Electrobun パッケージ管理上のバージョン。`electrobun.config.ts` の `app.version` と揃えて管理する | `0.0.4` |

Windows インストーラ／画面表示用のバージョン（root `package.json`）と、Electrobun アプリ本体のバージョン（`desktop/` 配下）は異なる数値体系で運用されている。

---

## 9. リソース（アイコン）

アイコン素材は `desktop/assets/` に配置する。

```
desktop/assets/
  icons8-コーヒーカップ-100.png  # ソース画像（Icons8）
  icon.ico                        # Windows 用（100px PNG から生成）
  icon.iconset/                   # macOS 用（sharp で各サイズ生成済み）
    icon_16x16.png
    icon_16x16@2x.png
    icon_32x32.png
    icon_32x32@2x.png
    icon_128x128.png
    icon_128x128@2x.png
    icon_256x256.png
    icon_256x256@2x.png
    icon_512x512.png
```

`electrobun.config.ts` の `build.win.icon`（`assets/icon.ico`）／`build.mac.icons`（`assets/icon.iconset`）でビルド時に埋め込む。

### アイコンを差し替える場合

より高解像度のソース画像（512px 以上推奨）を入手したら、以下のスクリプトで再生成する。

```bash
# Windows（Bun）: icon.ico を再生成
bun -e "
const pngPath = 'desktop/assets/<新しい画像>.png';
const icoPath = 'desktop/assets/icon.ico';
const pngData = await Bun.file(pngPath).arrayBuffer();
const pngBytes = new Uint8Array(pngData);
const buf = new ArrayBuffer(6 + 16 + pngBytes.length);
const view = new DataView(buf);
view.setUint16(0, 0, true); view.setUint16(2, 1, true); view.setUint16(4, 1, true);
view.setUint8(6, 0); view.setUint8(7, 0); view.setUint8(8, 0); view.setUint8(9, 0);
view.setUint16(10, 1, true); view.setUint16(12, 32, true);
view.setUint32(14, pngBytes.length, true); view.setUint32(18, 22, true);
new Uint8Array(buf, 22).set(pngBytes);
await Bun.write(icoPath, buf);
"

# macOS: iconset を再生成（macOS 環境で実行）
mkdir -p desktop/assets/icon.iconset
sips -z 16 16   <ソース>.png --out desktop/assets/icon.iconset/icon_16x16.png
sips -z 32 32   <ソース>.png --out desktop/assets/icon.iconset/icon_16x16@2x.png
sips -z 32 32   <ソース>.png --out desktop/assets/icon.iconset/icon_32x32.png
sips -z 64 64   <ソース>.png --out desktop/assets/icon.iconset/icon_32x32@2x.png
sips -z 128 128 <ソース>.png --out desktop/assets/icon.iconset/icon_128x128.png
sips -z 256 256 <ソース>.png --out desktop/assets/icon.iconset/icon_128x128@2x.png
sips -z 256 256 <ソース>.png --out desktop/assets/icon.iconset/icon_256x256.png
sips -z 512 512 <ソース>.png --out desktop/assets/icon.iconset/icon_256x256@2x.png
sips -z 512 512 <ソース>.png --out desktop/assets/icon.iconset/icon_512x512.png
iconutil -c icns desktop/assets/icon.iconset  # .icns も生成される
```

### 既知の問題

Windows ビルド時、`rcedit` の依存解決に失敗し `launcher.exe`／`bun.exe` へのアイコン埋め込みが警告付きでスキップされる場合がある（`Cannot find module '...\node_modules\rcedit\package.json'`）。Electrobun が Bun のグローバルキャッシュルートから実行される際、ローカルの `node_modules/rcedit` を解決できないことが原因（#846）。Electrobun 1.18.1・1.18.4-beta.3 の時点では未解消。Electrobun 2.0.1（§10）で解消されるか確認する。

---

## 10. 今後の対応方針

### Electrobun 2.0.1 への移行（#1548）

1.x → 2.0 はメジャーバージョンアップで、新ビルドツールチェーン「Hutch」中心の設計に刷新されている。

- `desktop/package.json` の `electrobun` 依存を `2.0.1` に更新する
- `desktop/hutch.config.ts` を新設し、`packageManager: "bun"` を明示する（既存パッケージマネージャーを継続するため）
- `desktop/tsconfig.json` に `"extends": "./.hutch/devkit/tsconfig.json"` を追加する
- メインプロセスランタイムは Bun のまま維持する（Cottontail への切り替えは行わない）
- `desktop/electrobun.config.ts` は現状 2.0 での廃止フィールド（`build.targets`／`build.useAsar`／`build.cefVersion`）を使用していないため変更不要と想定するが、`npx electrobun config --env=stable` のバリデーションで実際に確認する
- **完了条件**: Windows・macOS 両方で `electrobun dev`／`build`／インストーラ（DMG）生成・実機インストール後の起動確認が完了すること。あわせて §9「既知の問題」の #846（アイコン埋め込みエラー）が解消されるかを確認する

---

## 改訂履歴

| 版 | 更新日 | 変更内容 |
| --- | --- | --- |
| 1.1 | 2026-08-27 | `main.js`（Electrobunランタイム本体）自身の孤児プロセス化対策を追加。起動フロー（§4）に自己ロックファイル方式のステップを追加し、§5の既知の課題を解消として記載を更新。今後の対応方針（§10）から対応済みとなった項目を削除（#1563） |
| 1.0 | 2026-08-27 | 初版作成。ビルドフロー・プラットフォーム別ディレクトリ構成・起動フロー・プロセス終了時のクリーンアップ・WebViewのキャッシュ制御・バージョン管理・リソース（アイコン）を集約。実装済みの対策（#1535: macOSパス解決バグ、#1561: 残留プロセス・WebViewキャッシュ）に加え、今後の対応方針（#1548: Electrobun 2.0.1移行、#1563: Windows `main.js` 残留プロセス対策、#846: アイコン埋め込みエラー）を記載（#73） |
