esbuildには--loaderというオプションがあります。このオプションを使うと、任意の拡張子のファイルをimportし、コンパイル時に依存関係に含めることができます。
esbuild−loaderの話ではありません。
この機能を使うと、例えば以下のようなTypeScriptコードをJavaScriptコードへコンパイルできるようになります。
// 任意の拡張子をimportできる
import css from "foo.css";
import txt from "foo.txt";
import bin from "foo.bin";
import png from "foo.png";
--loaderオプションの使い方
loaderオプションは以下のように使います。
$ esbuild esbuild app.js --bundle --loader:.png=dataurl --loader:.svg=text
上記のコマンドの--loader:.png=dataurlや--loader:.svg=text部分がloaderオプションです。
拡張子ごとに、--loader:.<拡張子>=<読み込み方法>の形で指定します。
loaderオプションで指定できる読み込み方法
loaderオプションには、拡張子ごとに読み込み方法を指定できます。
公式ドキュメントによると、以下のような読み込み方法が指定できます。
-
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という未知の拡張子をコンパイルしてみます。
import foo from "./hello.foo";
console.log(foo);
hello world!
何も指定しない場合
.fooなどという聞いたことない拡張子はコンパイルできないと言われました。当然ですね。
$ 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形式を指定してみます。
$ npx esbuild index.ts --bundle --outdir=dist --format=esm --loader:.foo=text
こちらは、import文だった所が文字列の"hello world!"に置き換えられました。
ファイルの内容がそのまま文字列としてbundleされます。
例えばsvgファイルなどに便利そうです。
// hello.foo
var hello_default = "hello world!";
// index.ts
console.log(hello_default);
--loader:.foo=binaryオプション
binary形式を指定してみます。
$ npx esbuild index.ts --bundle --outdir=dist --format=esm --platform=node --loader:.foo=binary
このオプションでは、import文がUint8Arrayに置き換えられます。
画像データなどのバイナリをimportしたい時に使えそうです。
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する場合に便利そうです。
$ npx esbuild index.ts --bundle --outdir=dist --format=esm --loader:.foo=base64
// hello.foo
var hello_default = "aGVsbG8gd29ybGQh";
// index.ts
console.log(hello_default);
--loader:.foo=dataurlオプション
こちらはdata URL形式に置き換わります。
data URLはURLの一種なので、fetch()の引数に渡したり、<img>タグのsrc属性に渡したりすることができます。
$ npx esbuild index.ts --bundle --outdir=dist --format=esm --loader:.foo=dataurl
// 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"というファイルが生成されており、このファイルへのパスが変数に入ります。
$ npx esbuild index.ts --bundle --outdir=dist --format=esm --loader:.foo=file
// hello.foo
var hello_default = "./hello-TO42AHOB.foo";
// index.ts
console.log(hello_default);
--loader:.foo=copyオプション
$ npx esbuild index.ts --bundle --outdir=dist --format=esm --loader:.foo=copy
こちらのオプションでは、import文が元の形のまま保持されます。
// index.ts
import foo from "./hello-TO42AHOB.foo";
console.log(foo);
注意点として、今回の場合のように、拡張子fooのようなファイルをimportしている時は、実行すると普通にランタイムエラーになるのでご注意ください。
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".foo"
使い道はあまりわかりません。
--loader:.foo=emptyオプション
こちらのオプションを使用すると、importの結果が空になります。
$ npx esbuild index.ts --bundle --outdir=dist --format=esm --loader:.foo=empty
生成されたコードは長いですが、実際に実行してみるとconsole.logで空のオブジェクトが出力されます。
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ファイル)を作り、拡張子に対する型を宣言してやることで、エラーを回避できます。
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環境のようなローカルファイルが読めない環境でも、バンドルしてしまえば通常の依存関係と同様に扱えるのが嬉しいポイントですね。