2023/05/16に公式バンドラBun BundlerをひっさげてBun v0.6.0が登場しました。
ということで以下はBun公式のBundler紹介記事、The Bun Bundlerの日本語訳です。
The Bun Bundler
Bunの高速ネイティブバンドラのベータ版が登場です。
CLIコマンドbun build
、もしくはJavaScriptのAPI Bun.build()
から使用可能です。
Bun.build({
entrypoints: ['./src/index.tsx'],
outdir: './build',
minify: true,
// その他設定があれば
});
bun build ./src/index.tsx --outdir ./build --minify
Reducing complexity in JavaScript
フォームフィールドの自動入力から始まったJavaScriptは、今ではロケットを宇宙に打ち上げる機器に活躍するところまできています。
当然の成り行きですが、JavaScriptエコシステムの複雑さは爆発しました。
TypeScriptのファイルはどうやって実行する?
本番用コードはどうビルドする?
そのパッケージはESMで動作する?
ローカル設定を反映するにはどうすればいい?
バージョン互換性をどう解決すればいい?
ソースマップはどうやって作れば?
複雑であるということは、すなわち時間がかかるということです。
npmパッケージのインストールは遅すぎます。
テストの実行は数秒かそれ以下であるべきです。
2003年当時、FTPサーバにファイルをアップロードするのは数秒でした。
2023年の今、ソフトウェアのデプロイに数分もかけさせられるのはどうしてですか?
何年も前から、JavaScriptを取り巻くあらゆる物事が非常に遅いことに、私は大変な不満を抱いていました。
ファイルを保存してから変更のテストが完了するまでの間にHacker Newsを読み終わってしまう。
何かが間違っています。
複雑さには、たしかに正しい理由があります。
BundlerとMinifierは、Webサイトの読み込み速度を向上させます。
TypeScriptのインタラクティブドキュメントは、開発者の生産性を向上させます。
タイプセーフは、バグがユーザに届いてしまう前に排除することに役立ちます。
依存パッケージのバージョン管理は、手動でコピーするよりも管理が容易です。
UNIX哲学である「ひとつのことをうまくやる」は、その「ひとつのこと」を行うツールが大量に孤立すると、破綻します。
それこそが、我々がBunを作った理由です。
そして今回、新たなBun Bundlerを発表できることをうれしく思います。
Yes, a new bundler
bun build
コマンドとBun.build
関数、および安定したプラグインシステムを揃えた新しいバンドラの登場により、バンドルはBunエコシステムの中核の一つとなりました。
Bunに独自のBundlerが必要であった理由は以下のとおりです。
Cohesiveness
Bundlerは、JSX・TypeScript・CSS・サーバコンポーネントなど、様々な要素をオーケストレーションして有効にするためのメタツールです。
現在、BundlerはJavaScriptエコシステムが複雑化してしまった最も大きな要因のひとつです。
BundlerをJavaScriptランタイムに組み込むことで、フロントエンドコードも、フルスタックコードも、よりシンプルに、より高速に解決できるようになります。
また近々、BundlerはBunのHTTPサーバであるBun.serve
とも統合する予定です。
これによって複雑なビルドパイプラインを、シンプルな宣言型APIで記述できるようになります。
Performance
これに異論はないでしょう。
Bunのコードベースには、ソースコードを超高速に解析するための下地が既に存在します。
ネイティブBundlerとの統合は、プロセス間通信のオーバーヘッドによりパフォーマンスが低下したりと困難でした。
最終的には、結果が物語ってくれます。
我々のベンチマークでは、Bunはesbuild
の1.75倍、Parcel2
の150倍、Rollup + Terser
の180倍、Webpack
の220倍の速度で動作しました。
Developer experience
既存のBundlerには、多くの改善の余地が存在しました。
Bundlerの設定に悩まされるのが好きな人は誰もいません。
Bun Bundlerの設定は曖昧さや予想外が起こらないように設計されています。
The API
現在、Bun BundlerのAPIは、設計上最小限のものです。
初期リリースの目標は、高速で動作し、安定しており、最新のユースケースに対応する、最小限の機能セットです。
以下が現在のオプション項目です。
interface Bun {
build(options: BuildOptions): Promise<BuildOutput>;
}
interface BuildOptions {
entrypoints: string[]; // 必須
outdir?: string; // デフォルト無し(メモリ)
target?: "browser" | "bun" | "node"; // デフォルト"browser"
format?: "esm"; // 今後 "cjs"/"iife"に対応予定
splitting?: boolean; // デフォルトfalse
plugins?: BunPlugin[]; // [] // see https://bun.sh/docs/bundler/plugins
loader?: { [k in string]: string }; // see https://bun.sh/docs/bundler/loaders
external?: string[]; // デフォルト []
sourcemap?: "none" | "inline" | "external"; // デフォルト "none"
root?: string; // デフォルト: computed from entrypoints
publicPath?: string; // 例: [http://mydomain.com/](http://mydomain.com/)
naming?:
| string // naming.entryと同じ
| { entry?: string; chunk?: string; asset?: string };
minify?:
| boolean // デフォルトfalse
| { identifiers?: boolean; whitespace?: boolean; syntax?: boolean };
}
多くのBundlerは、機能の完全性を追求するあまり、パフォーマンスを犠牲にしてしまいました。
我々は、その過ちを慎重に避けようとしています。
Module systems
モジュールはesm
フォーマットのみ対応しています。
今後iife
など他のモジュールシステムもサポートする予定です。
CommonJSは入力のみをサポートしており、出力はサポートしていませんが、もし要望が多ければcjs
の出力もサポートするかもしれません。
Targets
ターゲットはbrowser
・bun
・node
に対応しています。
・browser
JSXやTS等はJavaScriptにトランスパイルされます。
モジュールはexports
されます。
一部のNode.js APIはWebpack4と同じようにpolyfillされます。
・bun
BunとNode.jsのAPIはそのままです。
モジュールはBunデフォルトのアルゴリズムに解決されます。
・node
現在はbun
と同じです。
今後、Bun独自のAPIはpolyfillする予定です。
File types
以下のファイルタイプをサポートしています。
.js
・.jsx
・.ts
・.tsx
:JSとTS。
.txt
:プレーンテキスト。
.json
・.toml
:JSONにインライン化されます。
それ以外:すべてアセットとして扱われます。
// 入力
import logo from "./images/logo.png";
console.log(logo);
// 出力
var logo = "./images/logo.png";
console.log(logo);
Plugins
ランタイム同様、Bundlerもプラグインで拡張できます。
というより実は、ランタイムプラグインとBundlerプラグインは全く同じです。
import YamlPlugin from "bun-plugin-yaml";
const plugin = YamlPlugin();
// ランタイムプラグイン登録
Bun.plugin(plugin);
// Bundlerプラグイン登録
Bun.build({
entrypoints: ["./src/index.ts"],
plugins: [plugin],
});
Build outputs
Bun.build
関数はPromise<BuildOutput>
を返します。
interface BuildOutput {
outputs: BuildArtifact[];
success: boolean;
logs: Array<object>; // 詳細はドキュメント参照
}
interface BuildArtifact extends Blob {
kind: "entry-point" | "chunk" | "asset" | "sourcemap";
path: string;
loader: Loader;
hash: string | null;
sourcemap: BuildArtifact | null;
}
outputs
には、ビルドで生成されたすべてのファイルが入ってきます。
BuildArtifactはBlobインターフェイスを実装しています。
const build = await Bun.build({
/* */
});
for (const output of build.outputs) {
output.size; // ファイルサイズ
output.type; // MIME type
await output.arrayBuffer(); // => ArrayBuffer
await output.text(); // string
}
BuildArtifact
は、BunFile
と同じく直接new Response()
に渡すことができます。
const build = Bun.build({
/* */
});
const artifact = build.outputs[0];
// Content-Type は自動で決めてくれる
return new Response(artifact);
BuildArtifactのログ出力はデバッグしやすいように整形されています。
const build = Bun.build({/* */});
const artifact = build.outputs[0];
console.log(artifact);
/* 出力例
BuildArtifact (entry-point) {
path: "./index.js",
loader: "tsx",
kind: "entry-point",
hash: "824a039620219640",
Blob (114 bytes) {
type: "text/javascript;charset=utf-8"
},
sourcemap: null
}
*/
Server components
--server-components
フラグでReact Server componentsに実験的対応しています。
今後追加ドキュメントやサンプルを公開予定です。
Tree shaking
Bun Bundlerは未使用コードの削除機能があり、常に有効です。
またpackage.json
のsideEffects
をサポートしています。
これはパッケージに副作用が無いことを示すフラグであり、より積極的な不要コード削除を行います。
__PURE__
アノテーションに対応しています。
function foo() {
return 123;
}
/** #__PURE__ */ foo();
foo()
は副作用がないので、結果はこうなります。
詳細はWebpackのドキュメントを参照してください。
process.env.NODE_ENV
と--define
にも対応しています。
これらは、条件によってコードを分岐するためによく使用されます。
if (process.env.NODE_ENV !== "production") {
module.exports = require("./cjs/react.development.js");
} else {
module.exports = require("./cjs/react.production.min.js");
}
process.env.NODE_ENV==production
であれば、Bun Bundlerは不要なifを消し去ります。
ES Module tree-shaking
Bun BundlerはESMから不要なコードを自動的に削除します。
// entry.js
import { foo } from "./foo.js";
console.log(foo);
// foo.js
export const bar = 123;
export const foo = 456;
未使用のbarは削除されます。
var $foo = 456;
console.log($foo);
CommonJS tree-shaking
可能な場合に限りますが、Bun BundlerはCommonJSをESMに自動変換します。
// index.ts
import { foo } from "./foo.js";
console.log(foo);
// foo.js
exports.foo = 123;
exports.bar = "this will be treeshaken";
この場合、Bun Bundlerは、foo.js
をESMに変換します。
// foo.js
var $foo = 123;
// entry.js
console.log($foo);
CommonJSの性質上、解決が困難なケースも存在します。
// entry.js
export default require("./foo");
// foo.js
exports.foo = 123;
Object.assign(module.exports, require("./bar"));
// bar.js
exports.foobar = 123;
foo.js
を実行せずにexportを静的に決定することはできません。
さらに言うとObject.assign
はオーバーライド可能なので、静的解析は一般的に不可能です。
この場合、Bun BundlerはTree shakingを行わず、かわりにCommonJSのランタイムコードを挿入します。
var __commonJS = (cb, mod) => () => (
mod || cb((mod = { exports: {} }).exports, mod), mod.exports
);
// bar.js
var require_bar = __commonJS((exports) => {
exports.fooba = 123;
});
// foo.js
var require_foo = __commonJS((exports, module) => {
exports.foo = 123;
Object.assign(exports, require_bar());
});
// entry.js
var entry_default = require_foo();
export { entry_default as default };
Source maps
インライン・外部ソースマップの両方をサポートします。
const build = await Bun.build({
entrypoints: ["./src/index.ts"],
// *.js.mapファイルを作成
sourcemap: "external",
// ファイル内にsourceMappingURLを記載
sourcemap: "inline",
});
console.log(await build.outputs[0].sourcemap.json()); // => { version: 3, ... }
Minifier
JavaScriptではBundlerにMinifierはなくてはならないものです。
本リリースでは、全く新しいMinifierも導入されました。
単純にminify: true
で全てをMinifyするか、コンフィグで少し細かく設定することもできます。
{
minify?: boolean | {
identifiers?: boolean; // デフォルトfalse
whitespace?: boolean; // デフォルトfalse
syntax?: boolean; // デフォルトfalse
}
}
Minifierはデッドコードの削除、空白の削除、定数のインライン化などを行います。
// 入力
console.log("this" + " " + "text" + " will" + " be " + "merged");
// 出力
console.log("this text will be merged");
Jump in
bun create react
を更新し、内部でBun.build
を使うようになりました。
# React single-page app
bun create react ./myapp
# a Next.js-like app with a /pages directory
# with SSR and client-side hydration
bun create react-ssr ./myapp
Sneak peek: Bun.App
以下はプレビューです。
Bundlerは、より野心的なプロジェクトの足掛かりにすぎません。
Bun Bundler、HTTPサーバ、FileSystemRouterをひとつに合体させたスーパーAPI、Bun.App
を、今後数か月以内に公開する予定です。
ほんの数行で、Bunを使ったあらゆるアプリを簡単に実行できるようにすることが目標です。
new Bun.App({
bundlers: [
{
name: "static-server",
outdir: "./out",
},
],
routers: [
{
mode: "static",
dir: "./public",
build: "static-server",
},
],
});
app.serve();
app.build();
const app = new Bun.App({
configs: [
{
name: "simple-http",
target: "bun",
outdir: "./.build/server",
// Bundler設定
},
],
routers: [
{
mode: "handler",
handler: "./handler.tsx", // 自動include
prefix: "/api",
build: "simple-http",
},
],
});
app.serve();
app.build();
const projectRoot = process.cwd();
const app = new Bun.App({
configs: [
{
name: "react-ssr",
target: "bun",
outdir: "./.build/server",
// Bundler設定
},
{
name: "react-client",
target: "browser",
outdir: "./.build/client",
transform: {
exports: {
pick: ["default"],
},
},
},
],
routers: [
{
mode: "handler",
handler: "./handler.tsx",
build: "react-ssr",
style: "nextjs",
dir: projectRoot + "/pages",
},
{
mode: "build",
build: "react-client",
dir: "./pages",
// style: "build",
// dir: projectRoot + "/pages",
prefix: "_pages",
},
],
});
app.serve();
app.build();
感想
これまで登場した多くのツールはインストール速度を誇っていたわけですが、どうせインストールなんて最初以外ほとんどやらないんだから多少遅くてもいいだろ。
それよりも1行変更して反映30分みたいなクソ環境を経験しているとデプロイ速度の方が遙かに重要なわけですよ。
ということでJavaScriptとは一線を画する速度を誇るバンドラの登場です。
まあ言語仕様上仕方ないところもありますし、決してこれまでのツールが速度向上をサボっていたとかそう言いきれるわけでもないとは思いますが、ここまで圧倒的だと流石にもうJavaScript製バンドラは使っていられない感覚ですね。
そしてBun Bndlerは、まだまだただのバンドラで終わる気はないようです。
フロントエンドは相変わらず様々なツールを組み合わせなくてはならず非常に面倒です。
はやいところこれ一本だけで完結できるようになってほしいですね。