esbuildさわってみた
とりあえず依存が少なく、入れやすそうなパッケージから試してみる。
tscを置き換えてみる
インストール
npm install -D -E esbuild
TypeScript の項を読んでみるといくつか注意点。
- TSサポート(
.ts.tsx)はデフォルトで入っているので、特別必要なことはない - ただし、babelと同じくトランスパイルはしてくれるが、型チェックはしてくれないので、
tsc --noEmitを別で走らせてチェックはした方がよい - TSにしか存在しないシンタックスもちゃんと動作する
- tsconfig.jsonで
isolatedModulesを有効にする必要がある- ファイルごとに並列でビルドするので、ビルド時にimportされたものが型なのか値なのか判別できないらしい
- tsconfig.jsonで
esModuleInteropを有効にする必要がある- tscがデフォルトでesmをcommonjsに変換するため
- 以下は サポートされない
-
emitDecoratorMetadata- TSの型情報に依存した出力が必要なため
-
const enum- コンパイル時に普通の
enumに変換される
- コンパイル時に普通の
-
tsconfig.jsonについて
esbuild実行時に tsconfig.json を探して、実行時のパスからディレクトリを上に探索する。
ただし、esbuildは tsconfig.json のうち次のプロパティしか参照しない
- baseurl
- extends
- importsNotUsedAsValues
- jsxFactory
- jsxFragmentFactory
- paths
- useDefineForClassFields
ほかのプロパティは全て無視される。
tsconfig.json の target オプションもシカトされるが、
はesbuild自体の方の target オプションで指定することができる。
また、esbuildの --bundle オプションを有効にする必要がある。
Node環境向けにビルドしてみる
ビルドスクリプトの用意
npms scripts内で esbuild を直接指定してもよいが、ビルド用のスクリプトを用意することが推奨されているので、build.js を作って build: node build.js を npm scriptsに指定して実行する。
メモ
- platformを指定しないと一部のコンパイルが失敗する
- Node環境の場合は、
platform: 'node'を指定すればよい
- Node環境の場合は、
- 外部ライブラリを使用している場合は、
externalに指定する必要がある - build時の警告が鬱陶しい場合は、
logLevel: 'error'などを指定すれば大人しくなる -
watchオプションでファイルを監視してくれる
最終的にできたビルドスクリプト
const { build } = require('esbuild')
const watch = process.env.WATCH === 'true'
console.log(`Building @dawlet/utils with esbuild.\nWatch Option: ${watch}`)
build({
entryPoints: ['./src/'],
bundle: true,
platform: 'node',
outfile: 'lib/index.js',
external: [
'electron'
],
watch,
logLevel: 'error'
})
entrypointに複数のファイルを指定したい
entryPointsに ./src/* のようなglobパターンを渡すことはできないらしい。
> error: Could not resolve "./src/*.ts" (glob syntax must be expanded first before passing the paths to esbuild)
こういうエラーが出る。
経緯
この辺のissue で議論があり、globの展開はshellが行なっているので、shell-agnosticにesbuildを保つため、globの展開は自分でやれ とのこと。
解決策
esbuildのauthorの勧める方法としては、glob ライブラリを使うとよい。
ちなみに、outbase を指定すると、ソースとなるディレクトリ構造を保って出力先にコンパイルされるので、./src などを指定しておくとよさそう。
なので、
const { build } = require('esbuild')
const glob = require('glob')
const entryPoints = glob.sync('./src/**/*.ts')
build({
entryPoints,
platform: 'node',
outbase: './src',
outdir: './lib'
})
とすればできた。
おまけ
誰かがplugin作ってそうだなと思ったら、それっぽいのあった
Webpackに取り込んでみる
esbuild-loader というのを使うと、webpackにloaderとしてesbuildを組み込めるらしい。
元々 ts-loader を使っている場合であれば、
{
test: /\.tsx?$/,
- use: 'ts-loader'
+ use: 'esbuild-loader',
}
これが最小の変更。
ただし、tsxを使っている場合であれば、その設定を渡す必要があるので、
{
test: /\.ts(x?)$/,
use: [{
loader: "esbuild-loader",
options: {
loader: "tsx"
}
}]
}
こんな感じで動いた。
パフォーマンス比較
正直、自分の使っているプロジェクトでは、raw-loader を読み込んでいたり、monaco-editor-webpack-plugin を使っていたりと、tsのトランスパイル以外のところでボトルネックがあったので、期待していたほどのスピードアップは得られなかった。
before
56.50s
after
44.99s
約25.5%↑