esbuild を使おう

# esbuild を使おう

この記事はバージョン 0.17 時点での内容です。

## esbuild とは

esbuild は Webpack に代表されるモジュールバンドラの 1 つで、複数の Javascript ファイルなどを 1 つのファイルにまとめるなどの機能があります。 esbuild は Go で記述されているため Webpack などの多くの Javascript で記述されたバンドラより高速に動作します。また、Node.js 以外でも Deno でも動作します。

<blockquote>
Our current build tools for the web are 10-100x slower than they could be. The main goal of the esbuild bundler project is to bring about a new era of build tool performance, and create an easy-to-use modern bundler along the way.
<figcaption><a href="https://esbuild.github.io/">esbuild - An extremely fast bundler for the web</a>
</blockquote>

## テンプレート

半分自分用ですが貼ります。執筆現在 Deno 環境では Sass のバンドルなどが十分に機能しない（`@import` したファイルが watch されないなど）、プラグインが存在しないなどがあるため Node.js 環境をおすすめします。

- [Node.js 用テンプレート](https://github.com/Tsukina-7mochi/node-esbuild-template)
- [Deno 用テンプレート](https://github.com/Tsukina-7mochi/deno-esbuild)

## esbuild の使い方

### インストール

#### Node.js の場合

```shell
$npm install --save-dev esbuild
```

```javascript
const esbuild = require('esbuild')
```

#### Deno の場合

```typescript
// ↓ 最新のバージョンを確認してください
import * as esbuild from 'https://deno.land/x/esbuild@v0.17.14/mod.js';
```

### ビルド方法 (スクリプトからのビルド)

CLI からも実行可能ですが、スクリプトの方が柔軟性が高いです。基本的にはどちらの環境でもほぼ同様に記述できます。ただし Deno の場合は Deno の API の都合上 `esbuild.stop` を呼び出す必要があります。以下の例は Typescipt で記述します (Node.js 環境であれば ts-node を用いて実行可能です) 。

#### ビルドスクリプト

`config` は設定オブジェクトです (後述) 。

```typescript:build.ts (Node.js)
const ctx = await esbuild.context(config);

if(watch) {
  // watch モード (ファイルの変更を監視して自動でリビルド) の場合
  await ctx.watch();
  console.log('Watching...');

  // eslint-disable-next-line
  for await (const _ of process.stdin) {
    // 手動リビルド (例外は無視, コンソールには表示される)
    await ctx.rebuild().catch((_) => {});
  }
} else {
  // ビルドのみ
  await ctx.rebuild();
}
```

```typescript:build.ts (Deno)
const ctx = await esbuild.context(config);

if(watch) {
  // watch モード (ファイルの変更を監視して自動でリビルド) の場合
  await ctx.watch();
  console.log('Watching...');

  for await(const _ of readLines(Deno.stdin)) {
    // 手動リビルド (例外は無視, コンソールには表示される)
    await ctx.rebuild().catch((_) => {});
  }
} else {
  // ビルドのみ
  await ctx.rebuild();
}
```

#### 設定オブジェクト

ビルド設定を本番ビルド用とテストビルド用、その共通部分に分けて記述します (Node.js の場合 `posix` は `path` に読み替えてください)。本番用ビルドでも十分に速いので Webpack のように分ける必要はないかもしれません。

##### 共通設定
```typescript:esbuild.common.js
import * as esbuild from 'esbuild';
import { posix } from 'posix';

const srcPath = 'src';
const destPath = 'dist';
const cachePath = 'cache';

const config: Partial<esbuild.BuildOptions> = {
  entryPoints: [
    posix.join(srcPath, 'main.ts'),
    posix.join(srcPath, 'style/style.scss'),
  ],
  bundle: true,
  outdir: destPath,
  platform: 'browser',
}

export default config;
```

##### 本番設定

```typescript:esbuild.prod.js
import * as esbuild from 'esbuild';
import commonConfig from './esbuild.common.ts';

const config: esbuild.BuildOptions = {
  ...commonConfig,
  minify: true,
  sourcemap: 'linked',
}

export default config;
```

##### 開発設定

```typescript:esbuild.dev.js
import * as esbuild from 'esbuild';
import commonConfig from './esbuild.common.ts';

const config: esbuild.BuildOptions = {
  ...commonConfig,
  sourcemap: 'inline',
}

export default config;
```

### 実行

```shell:(Node.js)
node build.js
# ts-node の場合
npx ts-node build.ts
```

Deno の場合各種権限を与える必要があります (もはや `-A` でいいかも) 。

```shell:(Deno)
deno run --allow-run --allow-read --allow-write --allow-env --allow-net --allow-ffi --importmap import_map.json ./build.ts
```

## プラグイン

esbuild にも Webpack 同様プラグインがあり、 Webpack でいう loader の機能も持っています。また、プラグインは比較的容易に開発することができます。

簡単な例としてビルド結果を出力するプラグインを示します。

```typescript
const getTimeString = () => {
  const date = new Date();
  return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
};

const buildResultPlugin = (): Plugin => {
  let startTime = 0;
  let endTime = 0;

  return {
    name: 'build-result-plugin',
    setup(build) {
      // 開始時に実行される
      build.onStart(() => {
        startTime = Date.now();
      });

      // 終了時に実行される
      build.onEnd((result) => {
        endTime = Date.now();

        if (result.errors.length > 0) {
          console.log(`\x1b[1m${getTimeString()}\x1b[0m Build failed with ${result.errors.length} errors.`);
        } else {
          console.log(`\x1b[1m${getTimeString()}\x1b[0m Build succeeded in ${endTime - startTime}ms`);
        }
      });
    },
  };
};
```

`build.onStart` や `build.onEnd` に記述したリスナが実行されます。この他にも `build.onResolve` で import 解決時に呼び出されるコールバックや `build.onLoad` でリソースの読み込み時に実行されるコールバックを登録できます。

このとき設定ファイルには次のように記述することでプラグインを追加します。

```typescript
const config: Partial<esbuild.BuildOptions> = {
  entryPoints: [
    posix.join(srcPath, 'main.ts'),
  ],
  bundle: true,
  outdir: destPath,
  platform: 'browser',
  plugins: [
    buildResultPlugin(),
  ],
};
```

詳しくは https://esbuild.github.io/plugins/ を参照。