t28.dev

tsup を使う理由をドキュメントと実装から調べた

2023/7/1
Tech

tsup という TypeScript 製のライブラリ向けバンドラーが良さげ〜ってなったので、tsup がやってくれることを調べてみたメモ (v7.1.0 時点)。

tsup?

Bundle your TypeScript library with no config, powered by esbuild.

ref: https://www.npmjs.com/package/tsup

って書いてあるとおり、 tsup は esbuild のラッパー(バンドラー)。with no config という部分から「ライブラリ向けの esbuild の設定をいい感じにやってくれるんだな〜」と想像出来るけれど、

  • 具体的に何をやってくれているの?🤔
  • わざわざ依存関係を増やすほどのメリットあるの?🤔

って部分が気になる。

tsup を採用している OSS

適当に検索して見つけた tsup の採用事例がこちら。

一方、なぜ tsup を採用したか、各 OSS のモチベーションは PR を見ても分からなかった…(OSS が依存するライブラリってどうやって選ばれるんだろう…?)。

実装を軽く読んでみる

エントリーポイント

$ cat package.json | jq '.main'
"dist/index.js"

$ cat package.json | jq '.bin'
{
  "tsup": "dist/cli-default.js",
  "tsup-node": "dist/cli-node.js"
}

cli-*.ts が複数あるけれど cli-main.ts が CLI 用実装の実体で、メインロジック(index.tsbuild()) を実行する役割を担っている。 cli-default.ts, cli-node.ts の違いは node だと skipNodeModulesBundle: true オプションを渡していることで、bundle から除外するモジュールを決定するために tsup が実装した esbuild plugin の振る舞いを少し変える。

メインロジック

index.tsbuild: (_options: Options) => Promise<void> で設定ファイルの読み込みや options の構築 (index.ts#L132-L143) をした後、

を並列で実行している (index.ts#L392)。

dtsTask

  • --dts オプションが有効のとき worker_threads(!) で並列に実行されるタスク。
  • タスクの実体は rollup.ts#L278-L288 で、rollup を実行する。
  • tsup がいくつかの rollup plugin を設定してくれている。

mainTasks

  • タスクの実体は esbuild/index.ts#L162-L244 で esbuild を実行する。
  • esbuild に渡すオプションを tsup のオプションに基づいていい感じに構築してくれたり、いい感じのデフォルト値として設定してくれたり、いい感じの tsup 独自 esbuild plugin を渡してくれたりしている。

ドキュメントを読んでみる

“Why tsup?” 的なセクションがなかったので、Usage を順番に眺める。

Bundle files

tsup src/index.ts ってやると、bundle された js ファイルが ./dist に出力される。

esbuild で同じようなことをする場合は esbuild src/index.ts --outdir=dist --bundle ってやるので、 tsup の with no config を最小限の CLI からも感じる。

Excluding packages

tsup は dependencies と peerDependencies をデフォルトでバンドルに含めない--external を使えばそれ以外のパッケージもバンドルから除外することが出来る。

esbuild も external オプション で依存関係を除外することが出来るけれど、デフォルトではバンドルする

Generate declaration file

tsup は --dts で型定義ファイルを出力することができる (rollup に出力させている)。

一方、esbuild は型定義ファイルの出力をサポートしていない。

Bundle formats

tsup は出力する js ファイルの形式として esm cjs iife をサポートしている。 tsup の format オブションは esbuild の format オプションとしてそのまま(例外あるけど)渡される (esbuild/index.ts#L164-L165)。

esbuild も esm cjs iifeサポートしている…というか、 tsup の format オプションを esbuild に渡している (esbuild/index.ts#L164-L165)。

加えて tsup は format を配列形式で渡せることが出来るため、複数の形式を出力するビルドが便利になっている。これは format の配列要素分、esbuild を実行することで実現している (index.ts#L249)。

Target environment

tsup の target オプションは esbuild の target オプションとしてそのまま渡される (index.ts#L172) ので、機能として同等。

esbuild は ES5 へ変換は未サポートだが、tsup は SWC を使って ES5 へ変換できる。

Compile-time environment variables

tsup ではビルドタイムで参照できる環境変数を定義することが出来る。 これは Node.js の形式 (process.env.*) や Vite (?) 形式 (import.meta.env.*) で定義した環境変数を esbuild の define を使って置き換える設定を tsup がやってくれているということ (esbuild/index.ts#L211-L218)。

Building CLI app

エントリーポイントのファイルに shebang が含まれているとき、そのファイルのパーミッションを 755 にしてくれる。ちょって便利。

Minify output

esbuild による minify の on/off とは別で、terser による minify を選ぶことが出来る。

余談だけれど、この機能は tsup 専用の terser plugin を実装することで実現していて、オプションの型や実装の構造が Vite と似ている。

Tree shaking

esbuild の tree shaking はバンドルする場合デフォルトで有効になりますが、いくつかの不具合の対策として、tsup 独自の --treeshake (plugins/tree-shaking.ts)を提供している。 このオプションを有効にすると、 rollup による tree shaking をビルドに組み込むことが出来る。

What about type checking?

esbuild は型チェックを行わない

tsup は --dts を有効にすると、実際の TypeScript コンパイラが実行されて宣言ファイルが生成され、型チェックも行われます。

Inject cjs and esm shims

  • cjs 環境でのみ使える __dirname
  • esm 環境でのみ使える import.meta.url

それぞれが異なる環境でも機能するためのコード (cjs_shims.js, esm_shims.js) が tsup によって挿入される(esbuild/index.ts#L220)。便利。

Copy files to output directory

--publicDir でビルド時に出力ディレクトリにコピーするファイルを設定できる。(多分、)便利。

ってことで

🥰「tsup 使う!」

ライブラリをビルドする上でどうせやることをある程度 1任せられるのはやっぱり楽だ。

Footnotes

  1. with no config と言いつつ、設定ファイルは普通に必要…。