ESbuildのloaderオプションを使って任意のファイルをimportする

esbuildには[--loader](https://esbuild.github.io/api/#loader)というオプションがあります。このオプションを使うと、任意の拡張子のファイルをimportし、コンパイル時に依存関係に含めることができます。

:::note warn
[esbuild−loader](https://www.npmjs.com/package/esbuild-loader)の話ではありません。
:::

この機能を使うと、例えば以下のようなTypeScriptコードをJavaScriptコードへコンパイルできるようになります。

```ts
// 任意の拡張子をimportできる
import css from "foo.css";
import txt from "foo.txt";
import bin from "foo.bin";
import png from "foo.png";
```


## --loaderオプションの使い方

loaderオプションは以下のように使います。

```shell
$ esbuild esbuild app.js --bundle --loader:.png=dataurl --loader:.svg=text
```

上記のコマンドの`--loader:.png=dataurl`や`--loader:.svg=text`部分がloaderオプションです。

拡張子ごとに、`--loader:.＜拡張子＞=<読み込み方法＞`の形で指定します。

## loaderオプションで指定できる読み込み方法

loaderオプションには、拡張子ごとに読み込み方法を指定できます。

[公式ドキュメント](https://esbuild.github.io/content-types/)によると、以下のような読み込み方法が指定できます。

- `js`: JavaScriptファイルとして扱う
- `ts`: TypeScriptファイルとして扱う
- `jsx`: JSXファイルとして扱う
- `tsx`: TSXファイルとして扱う
- `json`: JSONファイルとして扱う
- `css` / `global-css` / `local-css`: CSSファイルとして扱う
- `text` / `binary` / `base64` / `dataurl`: import文をファイルの中身に置き換える
- `file`: import文をパス文字列に置き換える
- `copy`: import文はそのままだがパス部分だけ置き換える
- `empty`: 空のファイルとして扱う（無視する）

# loaderオプションを試してみる

`--loader`オプションを指定して実際にコンパイルして、出力ファイルがどうなるか見てみます。

今回コンパイルするのは以下のファイルです。
`.foo`という未知の拡張子をコンパイルしてみます。

```ts:コンパイル対象のファイル
import foo from "./hello.foo";
console.log(foo);
```

```txt:hello.fooの内容
hello world!
```

### 何も指定しない場合

`.foo`などという聞いたことない拡張子はコンパイルできないと言われました。当然ですね。

```shell
$ npx esbuild index.ts --bundle --outdir=dist
✘ [ERROR] No loader is configured for ".foo" files: hello.foo

    index.ts:1:16:
      1 │ import foo from "./hello.foo";
        ╵                 ~~~~~~~~~~~~~

1 error
```

### `--loader:.foo=text`オプション

`text`形式を指定してみます。

```shell
$ npx esbuild index.ts --bundle --outdir=dist --format=esm --loader:.foo=text
```

こちらは、import文だった所が文字列の`"hello world!"`に置き換えられました。
ファイルの内容がそのまま文字列としてbundleされます。
例えばsvgファイルなどに便利そうです。

```js:出力されたjsファイル
// hello.foo
var hello_default = "hello world!";

// index.ts
console.log(hello_default);
```

### `--loader:.foo=binary`オプション

`binary`形式を指定してみます。

```shell
$ npx esbuild index.ts --bundle --outdir=dist --format=esm --platform=node --loader:.foo=binary
```

このオプションでは、import文が`Uint8Array`に置き換えられます。
画像データなどのバイナリをimportしたい時に使えそうです。

```js:出力されたjsファイル
var __toBinaryNode = (base64) => new Uint8Array(Buffer.from(base64, "base64"));

// hello.foo
var hello_default = __toBinaryNode("aGVsbG8gd29ybGQh");

// index.ts
console.log(hello_default);
```

### `--loader:.foo=base64`オプション


こちらのオプションは`text`オプションに近いものの、import文のところが「ファイルの中身をbase64エンコードしたもの」になっています。
こちらのオプションも、バイナリデータをimportする場合に便利そうです。

```shell
$ npx esbuild index.ts --bundle --outdir=dist --format=esm --loader:.foo=base64
```

```js:出力されたjsファイル
// hello.foo
var hello_default = "aGVsbG8gd29ybGQh";

// index.ts
console.log(hello_default);
```

### `--loader:.foo=dataurl`オプション


こちらはdata URL形式に置き換わります。
data URLはURLの一種なので、`fetch()`の引数に渡したり、`<img>`タグの`src`属性に渡したりすることができます。

```shell
$ npx esbuild index.ts --bundle --outdir=dist --format=esm --loader:.foo=dataurl
```

```js:出力されたjsファイル
// hello.foo
var hello_default = "data:text/plain;charset=utf-8,hello world!";

// index.ts
console.log(hello_default);
```

### `--loader:.foo=file`オプション


`file`オプションでは、import文が**ファイルの中身ではなくファイルパスに置き換わります**。

出力ディレクトリを見ると、`"hello-TO42AHOB.foo"`というファイルが生成されており、このファイルへのパスが変数に入ります。

```shell
$ npx esbuild index.ts --bundle --outdir=dist --format=esm --loader:.foo=file
```

```js:出力されたjsファイル
// hello.foo
var hello_default = "./hello-TO42AHOB.foo";

// index.ts
console.log(hello_default);
```

### `--loader:.foo=copy`オプション


```shell
$ npx esbuild index.ts --bundle --outdir=dist --format=esm --loader:.foo=copy
```

こちらのオプションでは、import文が元の形のまま保持されます。

```js:出力されたjsファイル
// index.ts
import foo from "./hello-TO42AHOB.foo";
console.log(foo);
```

注意点として、今回の場合のように、拡張子`foo`のようなファイルをimportしている時は、**実行すると普通にランタイムエラーになる**のでご注意ください。

```shell:実行時のエラーメッセージ
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".foo"
```

使い道はあまりわかりません。

### `--loader:.foo=empty`オプション

こちらのオプションを使用すると、importの結果が空になります。

```shell
$ npx esbuild index.ts --bundle --outdir=dist --format=esm --loader:.foo=empty
```

生成されたコードは長いですが、実際に実行してみると`console.log`で空のオブジェクトが出力されます。

```js:出力されたjsファイル
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));

// hello.foo
var require_hello = __commonJS({
  "hello.foo"() {
  }
});

// index.ts
var import_hello = __toESM(require_hello(), 1);
console.log(import_hello.default);
```

こちらのオプションは、あえてファイルをbundleさせたくない時に有用そうです。

# TypeScriptエラーへの対処


ところで、上のように、`foo`といった未知の拡張子を持つファイルをimportすると、TypeScriptから怒られます。

```
Cannot find module './hello.foo' or its corresponding type declarations.ts(2307)
```

こういう場合は、型定義ファイル（`.d.ts`ファイル）を作り、拡張子に対する型を宣言してやることで、エラーを回避できます。

```ts:foo.d.ts
declare module "*.foo" {
  const data: string; // ここの部分はLoaderオプションに合わせて、Uint8Arrayなど適切な型を指定する。
  export default data;
}
```

この時、トランスパイル時に指定する`--loader`オプションの値に合わせて、exportされる変数の型を決める必要があります。
例えば、`binary`オプションを使用しているときは`Uint8Array`にする必要があります。

# まとめ

- esbuildの`--loader`オプションを使用すると、任意の拡張子のファイルをimportし、それをトランスパイルできます。
- `--loader`オプションでは拡張子ごとにオプションを指定できます。
- import文をどのように置き換えたいかに合わせて、`file` / `text` / `binary`など多様なオプションを設定することができます。
- TypeScriptエラーが出る場合は、`.d.ts`ファイルを記述することでエラーを抑制できます。

`--loader`オプション、JS / TSファイル以外をbundleする際に便利そうです。

フロントエンドなどではバンドルサイズが気になるので、`file`オプションを使うのがよさそうです。

バックエンドで使用する場合、FaaS環境のようなローカルファイルが読めない環境でも、バンドルしてしまえば通常の依存関係と同様に扱えるのが嬉しいポイントですね。