Yarn から Bun へ移行した実戦ログ: scripts・CI・Cloudflare をまとめて揃える by Codex


[前提]この記事は90%AIによって生成されました。



このリポジトリで Yarn から Bun に移行したとき、最初は `package.json` の scripts を置き換えるだけで終わると思っていました。実際はそうではなく、scripts、補助スクリプト、CI、Cloudflare 側の実行コマンドまで一本化しないと、ローカルと本番が別物になります。

今回は、どこで躓いたかと、最終的にどのコードで解決したかを順番に書きます。

##先に結論

今回の移行で効いた順番は次の通りでした。

  1. packageManager と scripts の Bun 化
  2. scripts 内 spawn('yarn') の除去
  3. GitHub Actions の Bun 化
  4. Cloudflare Build/Deploy command の Bun 化
  5. 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:cfdeploy:cf を最初に Bun 化することでした。普段の bun run build だけ通っても、Cloudflare 導線が Yarn のままだと本番では失敗します。

##レイヤー2: scripts 内の Yarn 呼び出しを除去

package.json だけ変えても、scripts/*.tsspawn('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 可用性が落ちる

##次に別プロジェクトでやるなら

次回は初日で次を一気にやります。

  1. packageManager 固定
  2. scripts 全置換
  3. scripts 内 spawn の全置換
  4. CI の setup/install/exec を Bun 化
  5. Cloudflare Build/Deploy command 同期
  6. rg で Yarn/NPM 残骸を全消し

この順で進めれば、移行途中の手戻りをかなり減らせます。

##まとめ

Yarn から Bun への移行は、コマンドを置き換える作業というより、実行経路を一本化する作業でした。

このリポジトリで効いた本質は、ローカル、CI、Cloudflare の3環境で同じコマンド体系に寄せたことです。ここが揃ってから、障害調査の再現性が上がり、デプロイ時の事故も減りました。

速度面のメリットもありますが、実務で一番効いたのは運用の単純化でした。