ローカル開発画面をMacアプリに — Electronではなくmanifest一枚とlaunchd
iframeでもラッパーアプリでもない。ブラウザがそのページに専用ウィンドウを付ける
毎朝開く個人用の画面がある。ローカルのRails開発サーバーで動いているページだ。これをブラウザのタブではなく、Macアプリのように使いたかった。まずElectronやTauriが浮かんだが、結局Webアプリのmanifest一枚とlaunchdひとつで終わった。
この通りにやれば、http://myapp.localhost:3000/ のような開発画面が、Dockアイコン付きの独立したウィンドウになる。
何がアプリになるのか
iframeのように別のページの中に枠を作るのではない。ブラウザがそのページに専用のウィンドウを付ける。タブもアドレスバーもなく、Cmd+Tabにも別に出る。ウィンドウそのものがそのページだ。
ファイル三つと一行
manifestとアイコン二枚を public/ に置き、レイアウトの <head> に一行足す。
{
"name": "My Desk",
"short_name": "Desk",
"start_url": "/",
"display": "standalone",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
<link rel="manifest" href="/manifest.json">
つまずいたところが二つあった。
最初はファイル名を manifest.webmanifest にしていた。Railsの開発サーバーはこれを text/plain で返した。manifest.json に変えたら application/json になった。
インストールボタンを出すにはservice workerが必要だと思い込んでいた。違った。2026-10-01、macOS 14.6.1・Chrome 154で、service workerなしでもlocalhostのページに beforeinstallprompt が届いた。インストールすると ~/Applications/Chrome Apps/ の下にアプリができた。service workerがないのでキャッシュもなく、コードを直せばウィンドウにもそのまま反映される。名前やアイコンなどmanifestの変更だけは、Chromeへの反映が遅い。
インストールボタンを自分で付けるなら
beforeinstallprompt の prompt() は一回しか使えない。一度出して閉じると、リロードするまでボタンが反応しなくなる。機会を使い切ったら「アドレスバー右のインストールアイコン、または ⋮ メニューからページをアプリとしてインストール」と案内するように変えた。
let deferred = null;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferred = e;
installButton.hidden = false;
});
installButton.addEventListener('click', async () => {
if (!deferred) { showMenuHint(); return; }
deferred.prompt();
await deferred.userChoice;
deferred = null; // 二度は使えない
});
インストールの確認ダイアログはページの外、ブラウザのUI側に出る。だから拡張機能のような自動化では、最後の「インストール」を押せない。そこは人が押す。
サーバーはlaunchdが立てる
アイコンを押してもサーバーが止まっていたら意味がない。ログイン時にlaunchdが開発サーバーを立てるようにした。
うちのリポジトリの起動コマンド(make lad)は、ポートを掴んでいるプロセスを先に kill -9 する。だからポートが空いているときだけ起動するように包んだ。launchdのジョブが終わるときに立てたサーバーまで道連れにならないよう、AbandonProcessGroup も有効にした。
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string><string>-lc</string>
<string>lsof -iTCP:3000 -sTCP:LISTEN || (cd ~/src/myapp && make lad)</string>
</array>
<key>RunAtLoad</key><true/>
<key>AbandonProcessGroup</key><true/>
Electron・Tauriと比べると
| 方法 | 必要なもの | 得られるもの |
|---|---|---|
| manifestでインストール | ファイル3つ + link一行 | Dockアイコン、アドレスバーのないウィンドウ、Cmd+Tab |
| Tauri・Electronのラッパー | 新しいプロジェクト・ビルド・署名 | メニューバー、グローバルショートカット、ウィンドウ状態の記憶 |
| サーバーごと同梱した独立アプリ | Rails・DBを同梱 | オフライン |
ラッパーをかぶせても、中身は同じ画面だ。メニューバーのアイコンやグローバルショートカットがどうしても要るならTauriにする。Dockから押してすぐ開ければいい個人用ツールなら、manifestで足りる。ビルドと署名のパイプラインをもうひとつ抱えても、管理コストが増えるだけだ。
まだ確かめていないこともある。Safariの「Dockに追加」が同じmanifestで動くか、サーバーが止まっているときにアプリのウィンドウが何を表示するかは見ていない。
社内の管理画面や個人用ツールを「アプリっぽく」使いたいけれど、どこまで作ればいいか分からないなら、https://www.negabaro.com/contact から聞いてほしい。
動作フロー
public/にmanifest.jsonとアイコン192・512を置き、<head>にlinkを一行
Chromeでページをアプリとしてインストール → Dockアイコンと専用ウィンドウ
launchdがログイン時、ポートが空いているときだけ開発サーバーを立てる
メリット
- ✓ ビルド・署名なし、コードの変更がそのまま反映
- ✓ service workerなしでもインストールできた
デメリット
- ✗ メニューバーアイコン・グローバルショートカットはない
- ✗ インストールの最後のボタンは人が押す