← 記事一覧に戻る

Bun で npm パッケージが動かない・インストールエラーになる時の5つの原因と対策【node-gyp / ネイティブモジュール / Node.js互換性】

Bun でパッケージが動かない・よくある原因

2024年にv1.0がリリースされたBunは、Node.jsより高速なJavaScriptランタイムとして注目を集めています。しかし「Node.jsのdrop-in replacement」を謳いながらも、実際にはすべての npm パッケージが動くわけではありません。

# Bun への移行時によく見るエラー例
error: Failed to install dependencies
error: node-gyp failed
error: Cannot find module 'X'

この記事では、Bun で npm パッケージが動かない・インストールエラーになる代表的な5つの原因と、それぞれの対処法を解説します。

原因1: node-gyp 依存のネイティブモジュールが動かない

最も多いのが、C++で書かれたネイティブアドオンへの依存です。bcryptsharpcanvasnode-sass などのパッケージは、インストール時にネイティブコードをコンパイルします。

$ bun install
error: Installation failed
  bcrypt@5.1.1: Failed to install due to node-gyp build failure
  python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])' returned exit status 1

対処法: 純粋なJS/WASM実装の代替パッケージに切り替える

元のパッケージ Bun互換の代替 理由
bcrypt bcryptjs 純粋なJavaScript実装
sharp (ネイティブ) sharp v0.32.6以降 Bun対応済み
canvas @napi-rs/canvas NAPI-RS経由でBun対応
node-sass sass (Dart Sass) 純粋なJS実装
# bcrypt を bcryptjs に切り替える例
bun remove bcrypt
bun add bcryptjs
bun add -d @types/bcryptjs
// 変更前
import bcrypt from 'bcrypt'

// 変更後(APIは同じ)
import bcrypt from 'bcryptjs'

const hash = await bcrypt.hash('password', 10)
const isValid = await bcrypt.compare('password', hash)

ネイティブモジュールを特定するコマンド:

# インストール済みの .node ファイルを探す
find node_modules -name "*.node" 2>/dev/null

Bun 1.1以降では多くのネイティブモジュールが動くようになっていますが、すべてではありません。まずネイティブビルドが必要なパッケージを特定するところから始めましょう。

原因2: CommonJS と ESM の混在でエラーが出る

Node.jsは require() (CommonJS) と import (ESM) の混在を様々な形でサポートしますが、Bunではより厳格に扱われることがあります。

# よくあるエラー
error: require() of ES Module not supported
ReferenceError: require is not defined in ES module scope

Bunのモジュール解決ルール:

.mjs → 常にESMとして扱う
.cjs → 常にCommonJSとして扱う
.js  → package.json の "type" フィールドに従う
.ts  → Bunが直接処理(ESMとして扱う)

対処法: package.jsontype フィールドを明示する

{
  "type": "module",
  "scripts": {
    "start": "bun run index.ts"
  }
}

CommonJS ライブラリを ESM プロジェクトから使う場合は、createRequire を使います:

import { createRequire } from 'module'
const require = createRequire(import.meta.url)

// これで CommonJS モジュールを読み込める
const someLibrary = require('some-cjs-library')

原因3: Node.js 組み込みモジュールの一部が未実装

Bunは主要な Node.js 組み込みモジュールをサポートしますが、一部のモジュールや API は未実装または動作が異なります

# よくあるエラー
error: The "vm" module is not supported in Bun
error: domain.create is not a function

主な互換性状況 (2025年現在):

モジュール 状況
fspathos ✅ フル対応
crypto ✅ ほぼ対応 (Web Crypto API推奨)
httphttps ✅ 対応 (Bun.serve 推奨)
stream ✅ ほぼ対応
vm ⚠️ 部分対応
cluster ❌ 未対応
domain ❌ 非推奨のため未対応

対処法: Web標準APIへの書き換え(crypto の例)

// Node.js crypto の古い書き方(Bunで動かないケースあり)
import crypto from 'crypto'
const hash = crypto.createHash('sha256').update('data').digest('hex')

// Web Crypto API を使った書き換え(Bun/Node.js両対応)
const encoder = new TextEncoder()
const data = encoder.encode('data')
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')

特に vm モジュールを使うパッケージ(一部のテンプレートエンジンやサンドボックス実行ライブラリ)は問題が起きやすいです。依存先のライブラリが間接的に vm を使っているケースもあるため、エラーが出たらパッケージのソースを確認しましょう。

原因4: Express ミドルウェアを Bun.serve と混在させようとしている

高速化のために Bun.serve を使おうとして、Expressのエコシステムと混在させると問題が起きます。

// NG: Bun.serve と Express を混在させようとするパターン
import express from 'express'
const app = express()

Bun.serve({
  fetch(req) {
    // Express のミドルウェアを呼び出そうとしてもうまくいかない
    // res が Bun の Response と Express の res で型が違う
    app(req, res) // TypeError
  }
})

正しいアプローチ: Expressをそのまま使うか、Honoに移行する

// OK パターン1: Express をそのまま使う(互換性あり)
import express from 'express'
const app = express()
app.get('/', (req, res) => res.send('Hello'))
app.listen(3000)
// OK パターン2: Hono に移行(Bun ネイティブで推奨)
import { Hono } from 'hono'

const app = new Hono()
app.get('/', (c) => c.text('Hello'))

export default {
  port: 3000,
  fetch: app.fetch,
}

Hono は Bun ネイティブで動作し、hono/express アダプターで既存コードを段階移行することもできます。Express から完全に離脱するのが難しい場合、まずサーバー起動部分だけ Bun に任せる形が現実的です。

原因5: Jest テストが動かない(bun test との差異)

bun test は Jest 互換を謳っていますが、完全互換ではなく一部のテストが失敗します。

# よくある bun test エラー
error: jest.mock() is not supported at the module level
TypeError: jest.spyOn is not a function

主な差異と対処法:

// NG: jest.mock() でモジュール外変数を参照するパターン
const mockFn = jest.fn()
jest.mock('./api', () => ({
  fetchUser: mockFn,  // NG: ファクトリ関数スコープ外の変数は参照できない
}))

// OK: bun:test の mock.module() を使う
import { mock } from 'bun:test'

mock.module('./api', () => ({
  fetchUser: mock(() => Promise.resolve({ id: 1, name: 'テスト' })),
}))

基本的な describeitexpect は互換があります:

// bun test で動く Jest 互換の書き方
import { describe, it, expect } from 'bun:test'

describe('UserService', () => {
  it('ユーザーを取得できる', async () => {
    const user = await fetchUser(1)
    expect(user.name).toBe('テスト太郎')
  })
})

Jest のモジュールモック機能に強く依存しているテストは書き直しが必要です。プロジェクト全体を一気に移行するより、まず新規テストファイルだけ bun test で書き始めるのが現実的な移行ステップです。

Bun への段階的移行フロー

いきなり全部 Bun に移行しようとするのは危険です。以下の順番で段階的に移行するのが安全です:

1. パッケージマネージャーのみ移行(npm → bun install)
   ↓ 互換性問題が少なく効果が大きい
2. テストランナーを bun test に移行
   ↓ CI高速化の恩恵を受けやすい
3. 開発スクリプトを bun run に移行
   ↓ ts-node, tsx 不要になる
4. 本番サーバーを Bun ランタイムに移行
   ↓ リスクが最も高いため最後
# まずはパッケージマネージャーだけ Bun に切り替える
# 既存の package.json はそのまま使える
rm -rf node_modules package-lock.json
bun install  # これだけでnpmより大幅に高速になる

まとめ

症状 原因 対処法
node-gyp ビルドエラー ネイティブアドオン依存 純粋JS実装の代替パッケージに切り替え
require is not defined CJS/ESM混在 package.jsontype: "module" 設定
vm module is not supported Node.js API 未実装 Web標準APIへ書き換え
Express + Bun.serve でエラー APIレイヤーの混在 Hono移行か Express のみで統一
jest.mock() が動かない bun test との差異 mock.module() に書き換え

Bunの互換性は急速に改善されていますが、既存のプロジェクトをそのままBunに移行しようとすると必ず何らかの問題が出ます。まず bun install だけ試してみて、速度の恩恵を受けながら徐々に移行範囲を広げていくのがおすすめです。