[前提]この記事は90%AIによって生成されました。
このリポジトリで Yarn から Bun に移行したとき、最初は `package.json` の scripts を置き換えるだけで終わると思っていました。実際はそうではなく、scripts、補助スクリプト、CI、Cloudflare 側の実行コマンドまで一本化しないと、ローカルと本番が別物になります。
今回は、どこで躓いたかと、最終的にどのコードで解決したかを順番に書きます。
##先に結論
今回の移行で効いた順番は次の通りでした。
packageManagerと scripts の Bun 化- scripts 内
spawn('yarn')の除去 - GitHub Actions の Bun 化
- Cloudflare Build/Deploy command の Bun 化
- Node バージョン固定と環境変数周りの耐障害化
この順で進めると、途中で「一部だけ Yarn 前提が残る」事故をかなり減らせます。
##実際に起きた失敗
まず Cloudflare Build で yarn build:cf が実行され、yarn が無くて即落ちしました。依存インストールは bun install で成功しているのに、build command だけ Yarn のままだったパターンです。
次に Node 20.5.1 環境で以下のエラーが出ました。
TypeError [ERR_IMPORT_ASSERTION_TYPE_MISSING]: Module ".../node_modules/wrangler/package.json" needs an import assertion of type "json"
これは Bun の問題ではなく、Cloudflare 側の Node 実行バージョンが古いことで出るエラーでした。Node を 20.19.2 へ寄せると解消しました。
さらに build の後半で Supabase 環境変数未設定により page data collect が失敗しました。ここも「必須 env が無いなら即 throw」設計だと CI で止まるため、実行時エラーへ遅延させる fallback が必要でした。
##レイヤー1: package.json を Bun 基準へ固定
まずここを明確にしました。
/Users/rrih/workspace/ro1/package.json
{
"packageManager": "bun@1.2.3",
"scripts": {
"prebuild": "bun run sync:content-snapshots",
"build:cf": "opennextjs-cloudflare build && node scripts/generate-cf-build-size-report.mjs",
"deploy:cf": "bun run build:cf && wrangler deploy --minify",
"quality-gate": "bun run quality-gate:basic"
}
}
ここで大事なのは build:cf と deploy:cf を最初に Bun 化することでした。普段の bun run build だけ通っても、Cloudflare 導線が Yarn のままだと本番では失敗します。
##レイヤー2: scripts 内の Yarn 呼び出しを除去
package.json だけ変えても、scripts/*.ts に spawn('yarn') が残っていると、結局そこから古い経路が呼ばれます。
今回は以下を Bun 実行へ統一しました。
/Users/rrih/workspace/ro1/scripts/build-and-cleanup.ts
const buildProcess = spawn('bun', ['run', 'build'], {
stdio: 'pipe',
shell: true
});
const tscProcess = spawn('bun', ['run', 'typecheck'], {
stdio: 'pipe',
shell: true
});
/Users/rrih/workspace/ro1/scripts/quick-build-check.ts
const tscProcess = spawn('bun', ['run', 'typecheck'], {
stdio: 'pipe',
shell: true
});
const lintProcess = spawn('bun', ['x', 'next', 'lint', '--quiet'], {
stdio: 'pipe',
shell: true
});
これでローカルの補助導線も Bun で閉じました。
##レイヤー3: GitHub Actions の Bun 化
ワークフローの install と実行を Bun に揃えないと、ローカルでは成功するのに CI だけ落ちる状態が残ります。
/Users/rrih/workspace/ro1/.github/workflows/detect-unused-tools.yml
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Analyze tool usage
run: bun x tsx src/scripts/analyze-tool-usage.ts
/Users/rrih/workspace/ro1/.github/workflows/fetch_data.yml
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Fetch Alpha Vantage Data
run: bun fetch_data.js
この統一で、CI の再現性がかなり上がりました。
##レイヤー4: Cloudflare 側の Build/Deploy command の同期
Cloudflare 側の Build command が Yarn のままだと、リポジトリを Bun 化しても失敗します。最終的に以下へ合わせる必要がありました。
- Build command:
bun run build:cf - Deploy command:
bun x wrangler deploy
この設定を入れた後、Cloudflare Logs 上でも install → build → opennext bundle → deploy の流れが一貫して Bun になりました。
##レイヤー5: Node バージョンと env 耐障害性
###Node バージョン
Cloudflare 側 Node 20.5.1 では wrangler 周辺で import assertion 系エラーが出たため、Node 20.19.2 に上げました。
合わせて、リポジトリ内で古いバージョンが書かれていた参照箇所も揃えました。
###Supabase env が無いときの設計
build 時に Supabase env が無くても、全ページを即死させないようにクライアント生成を fallback 化しました。
/Users/rrih/workspace/ro1/src/lib/supabase-client-fallback.ts
const MISSING_SUPABASE_ENV_MESSAGE =
"Missing Supabase env: set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY (or SUPABASE_URL/SUPABASE_ANON_KEY).";
export const createSupabaseClientOrThrowingProxy = ({ scope, url, key }) => {
if (url && key) {
return createClient(url, key);
}
warnMissingEnvOnce(scope);
return createThrowingClientProxy(scope);
};
/Users/rrih/workspace/ro1/src/lib/supabase.ts
const supabaseUrl =
process.env.NEXT_PUBLIC_SUPABASE_URL ?? process.env.SUPABASE_URL;
const supabaseAnonKey =
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? process.env.SUPABASE_ANON_KEY;
export const supabaseClient = createSupabaseClientOrThrowingProxy({
scope: "supabaseClient",
url: supabaseUrl,
key: supabaseAnonKey,
});
この形なら、build 中は警告ログに止めつつ、本当に必要な実行パスでだけエラーにできます。
##実際にやった確認手順
移行後は、次の3系統を毎回セットで確認しました。
- ローカル:
bun run typecheck,bun run build,bun run build:cf - Cloudflare: Build command と Deploy command が Bun になっているか
- リポジトリ:
rg -n "\\byarn\\b|npm run|npm install" package.json scripts .github README.md docs
とくに最後の検索は毎回効きました。置換漏れの早期検出にかなり使えます。
##このリポジトリ特有の学び
- Bun 化は package.json より scripts と CI のほうが事故源になりやすい
- Cloudflare は Build command だけ古い状態が残りやすい
- Node 実行バージョン差分は、依存不整合に見えて実は runtime 起因なことがある
- env 未設定時のエラー戦略を決めないと、build 可用性が落ちる
##次に別プロジェクトでやるなら
次回は初日で次を一気にやります。
packageManager固定- scripts 全置換
- scripts 内 spawn の全置換
- CI の setup/install/exec を Bun 化
- Cloudflare Build/Deploy command 同期
rgで Yarn/NPM 残骸を全消し
この順で進めれば、移行途中の手戻りをかなり減らせます。
##まとめ
Yarn から Bun への移行は、コマンドを置き換える作業というより、実行経路を一本化する作業でした。
このリポジトリで効いた本質は、ローカル、CI、Cloudflare の3環境で同じコマンド体系に寄せたことです。ここが揃ってから、障害調査の再現性が上がり、デプロイ時の事故も減りました。
速度面のメリットもありますが、実務で一番効いたのは運用の単純化でした。