Open5

esbuildさわってみた

yuichkunyuichkun

ゴール

自分の開発している Dawlet というプロジェクトでモノレポで開発しているTS + Electron のbuildが2分ぐらいかかるので、もっと早くしたい

yuichkunyuichkun

とりあえず依存が少なく、入れやすそうなパッケージから試してみる。

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.jsontarget オプションもシカトされるが、
はesbuild自体の方の target オプションで指定することができる。

また、esbuildの --bundle オプションを有効にする必要がある。

yuichkunyuichkun

Node環境向けにビルドしてみる

ビルドスクリプトの用意

npms scripts内で esbuild を直接指定してもよいが、ビルド用のスクリプトを用意することが推奨されているので、build.js を作って build: node build.js を npm scriptsに指定して実行する。

メモ

  • platformを指定しないと一部のコンパイルが失敗する
    • Node環境の場合は、platform: 'node' を指定すればよい
  • 外部ライブラリを使用している場合は、external に指定する必要がある
  • build時の警告が鬱陶しい場合は、logLevel: 'error' などを指定すれば大人しくなる
  • watch オプションでファイルを監視してくれる

最終的にできたビルドスクリプト

build.js
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'
})
yuichkunyuichkun

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作ってそうだなと思ったら、それっぽいのあった

esbuild-plugin-import-glob

yuichkunyuichkun

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%↑