今回のお題
Bun.js v1.0.12時点で出来なかった一部機能について、Bun.jsの最新版ではどうなったか...の検証記事
はじめに&宣伝
いきなり宣伝になってしまうのですが、本日(2024/05/19(金))発売の「Software Design 2024年6月号」誌において、「第2特集:[実証]Bun 次世代JavaScriptランタイムの実体に迫る」の記事を担当させて頂きました。
※ちなみに、担当したのは第1章と第3章です。
また記事内に記載の通り、「第3章:BunとNode.jsの徹底比較」の内容は、私が2023/11/19(日) に開催された、JSConf JP 2023 において発表した「Bun がメジャーリリースされたけど、本当にBun はNode.js に取って代わるのか?をAWS Lambda で検証してみた」の内容がベースになっています。(資料は以下)
なおこの資料の中で「困った点」として、当時のBun.jsでは出来なかった点を挙げています。(当時のバージョンはv1.0.12)

しかしあれからBun.jsは頻繁にアップデートが行われ、日本時間で今年の4/1(月)に(マイナーアップデートバージョンである)v1.1.0がリリースされました。*1 *2
そこで今回は「Software Design記事のおまけ」的な感じで、上記の「困った点」について『最新のBun.jsではどうなっているか』の検証結果を記事にしようと思います。
アジェンダ
上記「困った点」の3つについての検証です。
前提
Bun.jsはv1.1.2で検証しています。(最新版でも同じ結果になるはず)
最新版が使えない(packages/bun-lambdaが動かない)
どんな現象?
Bun.js公式リポジトリ には、packages/bun-lambdaという、Bun.jsをAWS Lambdaで動かす際に必要なパッケージがあるのですが*3、これがエラーになってしまい動かない、という現象です。
最新版では?
最新版では上記現象は改修されており、問題なく動作します。(なお権限系のエラーは別途対応が必要です。Software Designの記事にも対応方法を記載しています)
ビルドファイルが動かない
どんな現象?
ビルド時(bun build) にnpmモジュールをバンドルすると、ビルド後のjsファイル実行時にエラーが発生してしまい、動かないというものです。
正常に動かすためには、ビルド後のjsファイルの先頭に以下2行を明示的に追加する必要がありました。
outputFile.write('import { createRequire as createImportMetaRequire } from "module"; import.meta.require ||= (id) => createImportMetaRequire(import.meta.url)(id);\n\n');
最新版では?
こちらも最新版では上記現象は改修されており、上記2行を追加しなくても問題なく動作します。
モジュールモック未対応
どんな現象?
Bun.jsのテスト用モジュール(bun:test)で、npmモジュールなどのモジュールモックは未対応だった。
最新版では?
最新版ではモジュールモックにも対応しており、npmモジュールのモックも可能です。(詳しくは公式ドキュメント を参照。またサンプルソースを末尾に記載しておきます)
またマッチャーのJest互換性について、かなりの数のマッチャーが対応済ですが、まだ未対応のマッチャーも存在します。
ちなみに、bun:test でLambda関数のテストを実施する際の注意点について、以下に記載しておきます。
レスポンスをtoEqual() するだけではダメ
単体テスト時のレスポンスについて、await expect(response).toEqual(expected) みたいに、単に戻り値をtoEqual()しただけでは正しく判定できません。(おそらく、レスポンスがResponseクラス(のインスタンス)であることに関係していると思われます。) *4
とりあえずの回避策として、APIGatewayProxyResultなど、別の形式に変換することで対応できます。(末尾のサンプルソースを参照)
aws-sdk-client-mock-jest のマッチャーが使えない
Lambda関数内でAWS SDKを使用している場合、単体テストに aws-sdk-client-mock-jest を使用するケースも多いと思いますが、aws-sdk-client-mock-jest の一部マッチャーが使えません。*5
これはBunとJestで、expect関数の戻り値の型が違うのが原因です。
- Bun:Expect<T>型
- Jest:JestMatchers<T>型
また、そもそもBun読み込んだ時点でJestのexpect(declare const expect)が上書きされてしまい、aws-sdk-client-mock-jestとの整合性が取れなくなってしまうようです。
これについては今のところ有効な対策がなさそうで、強いて言えば「bun:test を使わない(Jestを使う)」くらいしかありません。(もしわかる人がいましたら教えてください)
まとめ
以上、最新のBun.jsによる検証結果でした。
基本的に「困ったこと」の現象は全て改修されており、最新版では問題なく使えることが確認できました。
実際Bun.jsは今でも頻繁にアップデートが実施されているので、これからもどんどん使い勝手が良くなっていくでしょうね。
今後のBun.jsの進化に期待です。
最後に繰り返しになりますが、Software Design 2024年6月号、よろしくお願いいたします。
それでは、今回はこの辺で。
参考:npmモジュールモック&レスポンス変換のサンプルソース
import { expect, test, describe, mock, jest as bun_jest } from 'bun:test'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb'; import { mockClient } from 'aws-sdk-client-mock'; import { handler } from '../lambda/index_bun'; import { APIGatewayEvent, APIGatewayProxyResult } from "aws-lambda"; // npmモジュールをモックする例。 // ここでは「uuid」をモックしています。 mock.module('uuid', () => { return { v4: bun_jest.fn(() => '1111-2222-3333-4444'), }; }); const dynamoDbOutput = { Type: 'treasure', Floor: '100' }; // aws-sdk-client-mock やRequest、Responseなどで必要になる設定 const testUrl = 'https://dummy.example.com'; const ddbMock = mockClient(DynamoDBDocumentClient); const dynamodb = new DynamoDBClient({}); DynamoDBDocumentClient.from(dynamodb); const eventData: APIGatewayEvent = { queryStringParameters: { "floor": "3" } } as unknown as APIGatewayEvent; const headers = new Headers({ 'Content-Type': 'application/json' }); const responseOptions = { status: 200, headers }; const responseString = JSON.stringify({ uuid: '1111-2222-3333-4444', item: dynamoDbOutput, }); const expectedResponse = new Response(responseString, responseOptions); describe('index_bun.handlerのテスト', () => { test('レスポンスが正しい事', async () => { ddbMock.on(GetCommand).resolves({ Item: dynamoDbOutput, }); const res = await handler(new Request(testUrl)); // ResponseインスタンスをAPIGatewayProxyResult型に変換 const [resResult, expectedResult] = await Promise.all([createApiGatewayProxyResultResponse(res), createApiGatewayProxyResultResponse(expectedResponse)]); expect(resResult).toEqual(expectedResult); }); }); // ResponseインスタンスをAPIGatewayProxyResultに変換する関数 async function createApiGatewayProxyResultResponse(res: Response): Promise<APIGatewayProxyResult> { const contextType = res.headers.get('Content-Type'); const body = await res.json(); const result = { statusCode: res.status, headers: { "Content-Type": contextType, }, body: JSON.stringify(body), } as APIGatewayProxyResult; return result; }