Bunを駆使して学ぶNode.jsモジュール開発

# 目次
- はじめに
- Bunとは
- 開発手順
    - 構成と環境
    - Bunインストール
    - 関数作成
    - クラスの作成
    - テスト実行
    - 型定義とbuildファイル作成
    - モジュールの利用
- 終わりに
- 参考文献



# はじめに
Nodeモジュールを開発する方法はいくつかありますが、Bunを使ってモジュール開発をしたので今回はそれを紹介します。(パッケージの公開は行いません)

# Bun とは
Bunは、特に「速度」と「効率性」を重視して設計された新しいランタイムです。

Nodeの場合、パッケージマネージャはnpmやyarnを使い、TypeScriptのトランスパイルはtypescriptパッケージを使います。

Bunの場合、自身がパッケージマネージャ(npm互換)やバンドラーの機能をもっています。
さらにTypeScriptのトランスパイルもデフォルトで可能で、トップレベルでawaitが使えます。
タスクランナーとしての使い方もできます。
```json:
{
  "name": "testapp",
  "scripts": {
    "clean": "rm -rf ./build"
  }
}
```
`bun run <コマンド>　`で実行できます。
そのほか各種詳細については[GithubのREADME](https://github.com/oven-sh/bun)で確認できます。

# 開発手順
## 構成と環境
Bunの開発環境はVSCodeのRemote-Containerを使用しています。
以降説明するサンプルは[こちらのGithubリポジトリ](https://github.com/bo-values/bun_sample_setup)をから参照できます。参考までに。

```txt:リポジトリ構成
├── .devcontainer
│   ├── devcontainer.json
│   ├── docker
│   │   └── bun
│   │       └── Dockerfile
│   └── docker-compose.yml
├── build
│   ├── index.d.ts
│   ├── index.js
│   └── packages
│       ├── calcs
│       │   ├── index.d.ts
│       │   └── index.js
│       └── clients
│           ├── index.d.ts
│           └── index.js
├── bunfig.toml
├── bun.lockb
├── package.json
├── README.md
├── src
│   ├── index.ts
│   ├── packages
│   │   ├── calcs
│   │   │   └── index.ts
│   │   └── clients
│   │       └── index.ts
│   └── tests
│       ├── calcs.test.ts
│       └── clients.test.ts
└── tsconfig.json

```

## Bunインストール
```bash:Bunインストール
$ curl https://bun.sh/install | bash

$ bun --version
1.0.x
```
なお前述した開発環境サンプルではDockerfileで環境を定義しているので、左下の「><」押下後、「コンテナで再度開く」を選択すると数分で開発環境が立ち上がります。

今回使用する基本的なコマンドは以下の通りです。
```bash:
$ bun run <指定>
スクリプト実行コマンド
js/tsファイルの実行、またはpackage.jsonに記述したスクリプトを実行

$ bun add <パッケージ名>
パッケージを指定してインストール

$ bun remove <パッケージ名>
インストールしたパッケージを削除

$ bun test <ファイルパス>
テスト実行
```
今回は使いませんが、テンプレートを指定して新規プロジェクトを作成する`bun create <指定>`もあるので、Bunでプロジェクト環境をたちあげることもできます。詳しくは[公式ドキュメント](https://bun.sh/docs/cli/bun-create)を参照ください。

## 関数作成
はじめに`bun install`を行い、開発環境を整えます。
```bash
$ bun install
bun install v1.0.19 (906f86d6)

Checked 6 installs across 7 packages (no changes) [119.00ms]
```

関数を作成します。
```ts:/workspace/src/packages/calcs/index.ts
interface Addition {
  /**
   * first number
   */
  first: number;

  /**
   * second number
   */
  second: number;
}

/**
 * Addition numbers
 */
export const addition = (props: Addition) => {
  const { first, second } = props;
  return first + second;
};

```
テストを作成します。
```ts: /workspace/src/tests/calcs.test.ts
import { expect, test } from "bun:test";
import { addition } from "../packages/calcs";

test("addition test", () => {
  const result = addition({
    first: 3,
    second: 25,
  });
  expect(result).toEqual(28);

  const failResult = addition({
    first: 10,
    second: 20,
  });
  expect(failResult).not.toBe(10);
});

```
## クラスの作成
クラスを作成します。
```ts: /workspace/src/packages/clients/index.ts
type Options = {
  /**
   * suffix for name
   */
  suffix?: string;
};

/**
 * Test Client class
 */
export class TestClient {
  _options?: Options;
  name: string;
  /**
   * TestClient constructor.
   *
   * @example
   * ```js
   * import {TestClient} from 'bun-sample'
   * const testClient = new TestClient();
   * ```
   *
   * @param options - Configuration options.
   */
  constructor(name: string, options?: Options) {
    this.options(options);
    this.name = name;
  }

  /**
   * Set options.
   *
   * @param options - Configuration options.
   */
  options(options?: Options): void {
    this._options = options;
  }

  /**
   * Get name
   */
  getName(): string {
    const name = this.name;
    const suffix = this._options?.suffix;
    return `${name}${suffix ?? ""}`;
  }
}
```
テストを作成します。

```ts: /workspace/src/tests/clients.test.ts
import { expect, test } from "bun:test";
import { TestClient } from "../packages/clients";

test("client test", () => {
  const name = "太郎";
  const testClient = new TestClient(name);
  const result = testClient.getName();
  expect(result).toBe(name);

  const suffix = "さん";
  const expectResultWithSuffix = `${name}${suffix}`;
  testClient.options({ suffix });
  expect(testClient.getName()).toBe(expectResultWithSuffix);
});

```
## テスト
テストを実行します。
実行ファイルを指定するときは```bun test <テストファイルパス>```で実行できます。
BunではテストをJestライクに定義しています。すべてJest仕様にしているわけではありませんが、サポート可否は[こちらのドキュメント](https://bun.sh/docs/test/writing)を参照してください。

テストを実行すると以下のように出力されます。
```bash
$ bun test
bun test v1.0.19 (906f86d6)

src/tests/calcs.test.ts:
✓ addition test [7.17ms]

src/tests/clients.test.ts:
✓ client test [56.44ms]

 2 pass
 0 fail
 4 expect() calls
Ran 2 tests across 2 files. [2.51s]
```
```bun test --cavarage```を実行するとカバレッジを表示することができます。また、```bunfig.toml```にデフォルトの設定を行うこともできるので今回は```bunfig.toml```を作成してデフォルトでカバレッジを表示してテストを実行するように設定します。

```toml: bunfig.toml
[test]

# always enable coverage
coverage = true
```
```bash
$ bun test
bun test v1.0.19 (906f86d6)

src/tests/calcs.test.ts:
✓ addition test [15.51ms]

src/tests/clients.test.ts:
✓ client test [31.61ms]
-------------------------------|---------|---------|-------------------
File                           | % Funcs | % Lines | Uncovered Line #s
-------------------------------|---------|---------|-------------------
All files                      |  100.00 |  100.00 |
 src/packages/calcs/index.ts   |  100.00 |  100.00 | 
 src/packages/clients/index.ts |  100.00 |  100.00 | 
-------------------------------|---------|---------|-------------------

 2 pass
 0 fail
 4 expect() calls
Ran 2 tests across 2 files. [3.77s]
```

## buildファイル作成
モジュールを作成するあたり、コンパイルの設定を行います。
こちらの[「tsconfig.jsonの主要オプションを理解する」](https://qiita.com/ryokkkke/items/390647a7c26933940470)がそれぞれのオプションがどのように機能するかをわかりやすくまとめているので参考にしてみてください。今回のサンプルでは以下のように設定しました。
```json:tsconfig.json
{
  "compilerOptions": {
    "esModuleInterop": true,
    "declaration": true,
    "declarationDir": "build",
    "outDir": "build",
    "baseUrl": "./",
    "paths": {
      "*": ["build/*"]
    },
    "types": ["bun-types"]
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", ".devcontainer", "src/tests"]
}
```
```json:package.json
{
  "name": "bun-sample",
  "description": "bun sample module",
  "module": "index.ts",
  "license": "UNLICENSED",
  "main": "./build/index.js",
  "types": "./build/index.d.ts",
  "scripts": {
    "test": "bun test --timeout 10000",
    "build": "bunx tsc",
    "clear:build": "rm -rf ./build"
  },
  "files": [
    "build"
  ],
  "devDependencies": {
    "bun-types": "latest"
  },
  "peerDependencies": {
    "typescript": "^5.0.0"
  }
}
```

`scripts`にタスクランナーとして`build`を設定しました。型定義を出力してみます。
```
$ bun run build
```
実行すると プロジェクトルートディレクトリに`build`フォルダと型定義ファイルが作成されます。

## モジュールの利用
作成したモジュールを外部で利用するには
- githubリポジトリを直接指定する
- パッケージを公開する

の２つがあります。
今回は直接リポジトリを指定してプロジェクトにインストールして確認します。

使用したいプロジェクトの`package.json > devDependencies`に以下のように登録します。
```
{
  "name": "testproject",
  "version": "0.1.0",
  "type": "module",
  .
  .
  .
  "devDependencies": {
    "bun-sample": "git+ssh://git@github.com:xxxxxx/xxxxx.git#main"
  },
}

```
`npm install`を実行して`node_modules`以下に`bun-sample`がインストールされます。
最後に、テストで記述したようにモジュールを呼び出して利用することができます。
```ts: index.ts
import {TestClient} from 'bun-sample'

const testClient = new TestClient("test")
.
.
.
```

# 終わりに
他の検証記事やコミュニティの議論を見ると、場合によってNodeやDenoが速かったりするので常にBunが最速というわけではないようです。また、古くからあるサービスや依存モジュールの解決などはまだ課題としてあるようです。（参考文献に紹介のLambda検証は特に）
とはいえ、トランスパイルがデフォルトで可能な点はTSでプロジェクトを作成したり、検証環境をサクッと整える分には有効な感じもします。

Bunのコンセプトは[All-In-One-Toolkit](https://bun.sh/blog/bun-v1.0#bun-is-an-all-in-one-toolkit)にある通り、JavaScript の優れた点をすべて捨てずに、遅さと複雑さを排除することです。既存ライブラリやフレームワークは引き続き動作させつつ、現在Node.jsの課題である複雑に積み重ねられた選択肢をよりシンプルにすることがBunの使命としているので、その点を丸く解決できることがBunの魅力でもあり今後も注目に値する点かなと感じます。

以上、Bunを使ってモジュール開発を行いました。
もっと多くに人に利用してもらいコミュニティ規模が大きくなるといいですね。

# 参考文献
- [Bun公式ドキュメント](https://bun.sh/docs)
- [tsconfig.jsonの主要オプションを理解する](https://qiita.com/ryokkkke/items/390647a7c26933940470)
- [Bun がメジャーリリースされたけど、本当にBun はNode.js に取って代わるのか？をAWS Lambda で検証してみた](https://speakerdeck.com/smt7174/bun-gameziyaririsusaretakedo-ben-dang-nibun-hanode-dot-js-niqu-tutedai-warunoka-woaws-lambda-dejian-zheng-sitemita)