Lambdaのトップレベルで非同期処理したいときのesbuild設定方法

Lambda の Node.js v22 ランタイムで動かしていたら躓いたのでメモです。
正確には、コンテナイメージを使用した Lambda なので、Dockerfile 内で build しています。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/typescript-image.html

さて、Lambda のトップレベルで非同期処理を書きたい場合があります。
例えば、下記のように Node.js と TypeScript の ORM である Prisma のデータ接続文字列を AWS のSecrets Manager から非同期取得し、PrismaClient に設定したいときです。

```typescript:prisma/index.ts（非同期処理をトップ階層に記述している例）
const prisma = new PrismaClient({
    // 関数内で @aws-sdk/client-secrets-manager を使ってシークレット情報を取得
    // https://docs.aws.amazon.com/ja_jp/sdk-for-javascript/v3/developer-guide/javascript_secrets-manager_code_examples.html
    datasourceUrl: await getDatasourceUrlFromSecretsManager()
});

export default prisma;
```

しかし、これだと実行時に下記のエラーになりました。

```bash
  "errorMessage": "Dynamic require of \"buffer\" is not supported",
  "trace": [
    "Error: Dynamic require of \"buffer\" is not supported",
    "    at file:///var/task/index.js:11:9",
    "    at ../../node_modules/.pnpm/@smithy+util-buffer-from@4.0.0/node_modules/@smithy/util-buffer-from/dist-cjs/index.js (file:///var/task/index.js:1024:25)",
```

そこで、下記の設定を esmodule に追加したら直りました。

```diff_json:package.json
{
    "scripts": {
-       "build": "esbuild --bundle --outfile=./dist/index.js --platform=node --target=node22 --format=esm ./src/index.ts"
+       "build": "esbuild --bundle --outfile=./dist/index.js --platform=node --target=node22 --format=esm --banner:js=\"import path from 'path'; import { fileURLToPath } from 'url'; import { createRequire as topLevelCreateRequire } from 'module'; const require = topLevelCreateRequire(import.meta.url); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename);\" ./src/index.ts"
    }
}
```

以下、ChatGPTにコードの説明をしてもらいました

> esbuildの--bannerオプションに指定されているコードは一種の「プロローグ」として、出力ファイルの先頭に挿入されるものです。このコードスニペットの目的は、Node.jsのESモジュール環境で手軽にCommonJS形式の特定の機能を使用できるようにすることです。具体的に、このコードは以下のことを行っています：

```js
import path from 'path';
```

> Node.jsのpathモジュールをインポートします。ファイルパスを操作するためのユーティリティを提供します。
```js
import { fileURLToPath } from 'url';
```
> urlモジュールからfileURLToPath関数をインポートします。これはファイルURLからファイルシステムパスを取得するための関数です。ESモジュールでは__filenameや__dirnameが自動で提供されないため、この関数が役立ちます。
```js
import { createRequire as topLevelCreateRequire } from 'module';
```
> moduleモジュールからcreateRequireをインポートし、それをtopLevelCreateRequireという名前で使用します。これは後でrequire関数をマニュアルで作成するために利用されます。

```js
const require = topLevelCreateRequire(import.meta.url);
```

> require関数を作成して現在のモジュール内で使用可能にします。ESモジュール内ではrequireがデフォルトで利用できないため、これを使ってCommonJSモジュールをインポート可能にします。

```js
const __filename = fileURLToPath(import.meta.url);
```

> 現在のモジュールのファイルパスを取得し、__filenameに格納します。ESモジュールでは__filenameが自動的に提供されないため、これを使い手動で定義します。
```js
const __dirname = path.dirname(__filename);
```

> 現在のディレクトリのパスを計算し、__dirnameに格納します。ESモジュールでは__dirnameも自動的には利用できないため、pathと組み合わせて手動で設定します。
まとめると、このコードスニペットはESモジュール環境でrequire, __filename, __dirnameを定義して、従来のCommonJSモジュールの一部の機能や利便性を利用できるようにするための手段を提供しています。これは、ESMとCJSの間の互換性を確保するために役立つことがあります。


## おわり

esbuild に banner オプションがあり、エントリーポイントより前に処理させることができることを知れました。
ESModules と CommonJS モジュールの関係を見直す良い機会になりました。

## 参考

https://stackoverflow.com/questions/77260658/sam-with-esbuild-mjs-file-failing-with-dynamic-require-of-net-is-not-supported

https://zenn.dev/junkor/articles/2bcd22ca08d21d