🎄

tsupでバンドルする / TypeScript一人カレンダー

2024/12/23に公開

こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2024の23日目です。昨日は『tsx TypeScript Execute』を紹介しました。

JavaScriptのモジュール史とバンドル文化

今回は少し昔話をします。

JavaScriptプログラミングにおいて、CommonJS (CJS)が登場するまでは「モジュール」という概念がほとんどありませんでした。これは15年以上前の話で、巨大なJSファイルにすべてのプログラムを書いたり、もし複数ファイルに分割しても最終的にはモジュール機構などないまま、単純にconcat(結合)する手法が主流でした。それは、あくまでも1990年代〜2000ゼロ年代はJavaScriptという言語がWebブラウザ上でWebページに動的な処理を追加する補佐的なスクリプト言語に留まっていたからです。

しかし2005年前後を境に巨大なWebアプリケーションが徐々に登場し始めました。Google Mapはその代表例です。そして2009年のNode.jsの登場とともに、CommonJSという仕様——特にそのなかのモジュール機構が普及し始めます。その後、BowernpmといったOSSエコシステムが形成され、その後の淘汰などを踏まえて、フロントエンド・アプリケーションの開発であってもnpmを一般的に利用する流れに収束していきました。

2010年代に入りJavaScriptプログラミングは隆盛を迎え、AltJSという一時のムーブメントを経て、2013年〜2015年頃にはES6、のちのES2015が普及を始ました。TypeScriptBabelなどの「任意のファイルを別のJavaScriptファイルに変換する」というトランスパイル文化も、この頃から芽生えました。

同様にBrowserifyWebpackなどの「バンドラー」も一般化し、細分化したjsファイル・tsファイルを一つにまとめる「バンドル文化」が確立されていきます。

この当時、ES2015で定義されたimportは構文の定義だけ先行し、実際、その構文に対してどう振る舞うかはトランスパイラ側の実装に委ねられていました。import構文を使いたければ、TypeScriptやBabelでCommonJSのコードに変換し、開発者はそれによって恩恵を受ける構図だったのです。

ESMが正式に実装される流れ

昔話は少し時が進み、2017年になります。

ESM(ECMAScript Modules)が徐々にランタイムレベルで正式にサポートされるようになりました。ブラウザが2017〜2018年頃にかけて徐々に対応を進めていきます。すなわち、Babelでの変換なくimport構文がネイティブで動作するようなサポートが開始しました。

さらに、Node.js 14以降、そしてTypeScript 4.5〜4.7の頃(2020〜2021年頃)に、さらにESM対応の動きが本格化しました。しかしこの頃といえば、React, Next.jsAngularといったWebアプリケーション・フレームワークといえば、すでに同梱のカスタマイズされたバンドラーを使うのが日常的となっており、特にこのESMサポートが日常を大きく変えるわけではありませんでした。

この「ESM抜きに、すでに文化形成が整ってしまっている」という現象は、我々のWebアプリケーション開発のパラダイムを早期から底上げしたという功績もあれど、同時に、仕様策定側からすれば想起されていた流れとは別の歪な方向にシフトしてしまっているという功罪がありました。

そして、2020年前後から現在に至るまでは「ESM移行過渡期」として、CJSとESMが入り混じり、エコシステム上に混乱が続きました。たとえばWebアプリケーション・フレームワークでは、独自のバンドラで最終的なJavaScriptを出力するため、「CJSかESMか」にそこまで意識を向けなくても動作する環境が主流であり、その結果CJSの資産は多く残存することになりました。

しかし一方で、Node.jsのOSSコミュニティはどんどんESM化を進め、CJSと両対応して互換性を維持するコミュニティもあれば、CJSを切り捨てESMのみに一本化するという姿勢をとるコミュニティもありました。互換性が断たれてしまうと、長い目でみればCJS時代からの開発者はエコシステム内における選択肢が狭まるという不幸な結果に繋がっていきます。

2024年現在の様子

2024年になった今「CJSとESMの整合性を保つための仕組み」がようやくひと通り出揃いました。筆者の印象としては、そろそろ過渡期も落ち着いたように見えます。

ただし、結果的に互換性をすべて維持する必要がある立場であるTypeScriptのコンパイラオプションは年々複雑化してしまい、拡張子の解決問題が厄介となりました。これは.jsimport文に含める必要がある欠点や、.cjs, .mjs, .cts, .mtsなどの多様な拡張子が一斉に増えたことによる煩雑さなどが挙げられます。

もちろんこうした背景や歴史の細部を完璧に把握しつつ、常に「現代の正解」や「今後の理想」を追い求めるエンジニアがいることは素晴らしいことです。しかし大半の人にとっては「なんでこんなに複雑なんだ…」「あまり深く考えずにサクッと使いたいのに…」という本音もあり得るでしょう。筆者自身、こうした記事を書くにあたって背景を習得したつもりではありますが、何も考えずに使えればそれでよいという気持ちは非常に共感できます。

2015年のES2015から約10年が経った今、モジュールのインポート文化は完全に定着し、バンドル文化にもあまり疑問を持たない開発者が大勢です。結果的に、ブラウザでESMが標準サポートされていても、なおフレームワーク付属のnpm run buildでバンドルされた最終成果物をデプロイするのが当たり前、という現場も多いことでしょう。

tsupが生まれた背景

ここまでを踏まえると、Node.jsでTypeScriptの開発をする場合、以下のような疑問や不満が出てきます。

  • tsconfig.jsonでの指定や、拡張子の取り扱いが面倒で、正解がいつまでも見えない
  • CJSかESMかを考えずに、ただTypeScriptファイルを書いていたい
  • 昔はtscだけでいいと思っていたのに、最近は読み込めるOSSや、エラーが出てしまうOSSなどバラけていて原因特定が煩雑

そんな問題をモダンに解決してくれるものが、今回紹介するtsupというバンドラーツールです。

前置きが長くなりました。ただ黙って「tsupは便利だから、みんな使おう」と勧めることもできたのですが、歴史的に複数のバンドラーが存在している状況で、なぜ現代にまた新しいバンドラーがひとつ増えたのかという経緯の把握は、必要な情報だと判断しています。

なぜtsupが優れているか

以前は、Node.jsのコードはフロントエンド向けプログラムとは異なるためWebpackを使わずにTypeScript Compilerのみでコンパイルして、そのまま実行・配布するというフローも不思議ではありませんでした。

しかし、npmでの配布において複数の互換性を考慮したり、特に現代ではCJS/ESMへの対応という、これまでにない煩雑さがつきまといます。また、他の開発環境に目を向けるとElectron向けの実装でmainプロセス向け、renderプロセス向けでビルドを分けたい場合などでも、バンドル需要は存在します。

「CJSのままでも、いま開発しているアプリは動くからいいや」と考える開発者もいるかもしれませんが、新しいOSSがCJSを切り捨てESMに一本化する流れも進んでおり、将来取り残されるリスクは把握することをおすすめしたいです。そこで、tsupはCJS/ESMの混在や拡張子問題をまとめて解決し、現代にとって最適なバンドルを手軽に生成してくれるのです。

省コンフィグ

tsupの大きな魅力の一つは、最低限のコンフィグで動作させられる手軽さにあります。設定ファイルをほぼ書かなくてもよく、index.tsmain.tsといったエントリポイントとなるファイルを渡すだけで基本的に問題なくバンドルしてくれます。

さらに、tsupは依存関係を非常に賢く扱います。たとえば、node_modulesに含まれていてバンドル不要なものは自動的に除外し、必要な依存だけを取り込むため、過剰に膨らんだ成果物を避けられます。Excluding packagesを参照してください。

拡張子周りの問題にも気を配っており、.cjs.mjs、さらには.cts.mtsといった多彩な拡張子を意識しなくて済むように設計されているのも大きなポイントです。これはBundle formatsを参照してください。

実際、tsupを使い始めると、従来のtsc / tsconfig.jsonや複雑なパイプラインの設定が必要なバンドラと比べて格段に楽になります。筆者もtsupを利用し始めてからは、tscnoEmitでの型チェック以外で使うことがほとんどなくなりました。それほどまでにシンプルかつモダンな体験を提供してくれるのがtsupだといえるでしょう。

tsupの裏側

tsupは内部でesbuildを利用しており、これはGo言語で実装されています。最近ではSWCのようにRust製のトランスパイラも登場し、JavaScript以外の処理系を活用することで圧倒的な速度を発揮するツールが多く登場しています。

この潮流は、高速化という大きなメリットがある反面、JavaScriptエンジニアにとってはOSSのソースコードを覗きにくい一面があるかもしれません。ただ、書籍『達人プログラマー(オーム社、Andrew Hunt, David Thomas著)』にもあるように、達人を目指すためには毎年一つ新しい言語を学ぶというスタンスで、来年はRustやGoを触ってみるのもいいかもしれません。

まとめ

今回は、JavaScriptのモジュール事情と、昨今のバンドラー事情、そしてtsupというツールの魅力を紹介しました。

バンドラーはその時々のあらゆる問題を解決するために進化を繰り返しており、歴史的・技術的背景がかなり複雑ですが、tsupのようなモダンなツールを使うと「複雑な背景をあまり気にしなくてよくなる」のは魅力です。OSSの世界は、こうした問題を解決するために多種多様なツールや仕組みが日々生まれ続けており、追ってみるのもとても面白いですね。

明日は『Biomeを使ったLintとフォーマット』

本日は『tsupでバンドルする』を紹介しました。明日は『Biomeを使ったLintとフォーマット』を紹介します。それではまた。

Discussion