Bun の非互換な拡張 API
https://gyazo.com/a71f94cf1c15e71313d8a2c16c59d1ab
Bun は WinterTC からの招待を無視し、標準から外れた拡張やまだプラットフォームで議論中の仕様を利便性のためだけに取り入れている。またエコシステムとして合意の取れていない実装をすることもある。
@jarredsumner: JS runtimes obsess about web standards but web standards orgs are incentivized to only care about browsers
https://pbs.twimg.com/media/GGjn7Q3a4AAXGKf.jpg
@lcasdev: @jarredsumner Just want to mention that we’ve invited you to WinterCG meetings for nearly 2 years now without any response from you - I think intentionally not participating and then saying “uh they don’t listen” is not very reasonable 😀
該当ツイートが消されているが、過去に以下のような発言をしたこともあるようだ。
https://offers.jp/media/event-report/a_6822#:~:text=調べれば出るかもしれませんが、「俺は幼稚園の徒競走みたいに手をつないでゴールをしたくない」のようなこと言っているので、見てみると面白いかもしれません。
これら非互換な API を使ってしまうと書いたコードが Node.js や Deno、Cloudflare Workers などで扱えず相互運用性の問題となる。また Bun の独自拡張を標準と勘違いしてしまい他ランタイムに対して誤ったバグ報告する例がいくつか見られ、JS コミュニティによくない影響が出てきている。
知らず知らずのうちに使ってしまわないようにまとめておく。なおバンドラーのため機能についてはここでは取り扱わない。
ECMAScript (JavaScript)
ES Modules のキャッシュ操作
Node.js や Deno では ES Modules と CommonJS が明確に区別されている。一方で Bun はモジュールを扱いやすくするために CommonJS と ES Modules を意図的に混ぜている。その結果、ES Modules で読み込んだモジュールのキャッシュを require.cache で操作出来るようになってしまっている。
code: js
import { foo } from "./foo.mjs";
const path = Bun.fileURLToPath(import.meta.resolve("./foo.mjs"));
console.log(require.cachepath);
仕様で Source Text Module Records である JavaScript モジュール(ES Modules)キャッシュ操作が出来ない。TC39 メンバーである ljharb さんからも ECMAScript にそのような仕様はないと教えてもらった。つまり Bun は JavaScript 仕様違反をしていると言っても過言ではなさそう。
@jordan.har.band: CJS modules aren’t governed by the spec, and there’s no mechanism in the spec to alter a hypothetical ESM cache.
ところで CommonJS によって読み込んだモジュールは require.cache でキャッシュを触ることが出来るが、これは Abstruct Module Records であると解釈することで一応 ECMAScript 仕様違反ではないと理解することができる。この辺りは uhyo さんのスライドが詳しい。
https://speakerdeck.com/uhyo/require-esm-toecmascriptshi-yang
Web 標準 API
AsyncIterable な console で標準入力から一行ずつ読む
In Bun, the console object can be used as an AsyncIterable to sequentially read lines from process.stdin.
https://bun.sh/docs/api/console
code: js
for await (const line of console) {
console.log(line);
}
もちろんこの機能は Console Standard にはない。
ReadableStream の "direct" タイプ
type: "direct" を指定することで ReadableStream がクローズされたあとに値を要請した際のエラーで具体的な情報が表示されるようになるらしい。標準の type: "bytes" と競合するがどうなるんだろう……。
Bun now provides more descriptive error messages when using ReadableStream with type: "direct". Instead of the generic "Expected Sink" error, you'll now see specific information about what went wrong, such as when trying to use a controller after the stream has been closed.
https://bun.sh/blog/bun-v1.2.5#improved-error-messages-for-readablestream
code: js
// Before: Generic "Expected Sink" error
// Now: Detailed error message
const stream = new ReadableStream({
type: "direct",
async pull(controller) {
controller.write("data");
// If you try to use the controller after pull() returns:
// Error: This HTTPResponseSink has already been closed.
// A "direct" ReadableStream terminates its underlying socket once async pull() returns.
},
});
Headers の getAll と toJSON メソッド
Set-Cookie HTTP フィールド(ヘッダー)対応のために Fetch Standard の Headers に getAll メソッドを追加しようという議論が起きていた。最終的に getSetCookie メソッドを追加することになった。
https://github.com/whatwg/fetch/issues/973
しかし Bun は結論が出る前に getAll と toJSON を独自実装した(toJSON をなぜ追加したのかはわからない)。
The Headers class now implements the .getAll() and .toJSON() methods. These are both technically non-standard methods, but we think it will make your life easier.
https://bun.sh/blog/bun-v0.3.0#new-methods-on-headers
code: js
const headers = new Headers();
headers.append("Set-Cookie", "a=1");
headers.append("Set-Cookie", "b=1; Secure");
console.log(headers.getAll("Set-Cookie")); // "a=1", "b=1; Secure"
console.log(headers.toJSON()); // { "set-cookie": "a=1, b=1; Secure" }
各ランタイムについての流れは Zenn に書いてある。
https://zenn.dev/pixiv/articles/3f607b19bb5e3a
Response のコンストラクタに AsyncIterable を渡す
code: js
const response = new Response(async function* () {
yield "hello";
yield "world";
}());
await response.text(); // "helloworld"
https://bun.sh/docs/api/streams#async-generator-streams
これは Fetch Standard に沿っていない。ただし Node.js (undici) でも同じように実行できてしまう問題がある。
https://github.com/nodejs/node/issues/49551
Web 標準では ReadableStream.from を経由することで対処できる。ただしこの辺りはまだ議論中(特にプリミティブである string に対してどうするか)で実装しているランタイムは少ない。
code: js
const response = new Response(ReadableStream.from(async function* () {
yield "hello";
yield "world";
}()));
await response.text(); // "helloworld"
https://github.com/whatwg/webidl/pull/1397
Fetch API の body オプションに Async Generator Function を渡す
Fetch Standard では body オプションに渡せるのは Blob, BufferSource, FormData, URLSearchParams, string, ReadableStream のいずれかだが、Bun は Node.js の Streams と Async Generator Functions を許容した。
You can now send fetch() Request bodies as streams. Previously, only Response bodies could be streamed in the client (Bun.serve() always supported both).
fetch()'s body supports streaming ReadableStream, Node.js streams and async generator* functions
https://bun.sh/blog/bun-v1.1.39#fetch-request-body-streams
code: js
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
fetch(url, {
method: "POST",
/* 'body' can be an async generator function */
body: async function* fn() {
const stream = client.messages.create(options);
for await (const {chunk: result} of stream) {
const { type, message } = chunk;
if (type === "succeeded") {
// stream message text chunk-by-chunk
yield message.content;
}
}
},
/* body can also be:
- body: new ReadableStream({...
- body: Bun.file(path)
- body: fs.createReadStream(path)
*/
);
前述した通り ReadableStream.from でラップするべき。
Fetch API の proxy オプション
In Bun, fetch supports sending requests through an HTTP or HTTPS proxy. This is useful on corporate networks or when you need to ensure a request is sent through a specific IP address.
https://bun.sh/guides/http/proxy
code: js
await fetch("https://example.com", {
// The URL of the proxy server
proxy: "https://username:password@proxy.example.com:8080",
});
Worker の "open" イベント
The "open" event is emitted when a worker is created and ready to receive messages. This can be used to send an initial message to a worker once it's ready. (This event does not exist in browsers.)
https://bun.sh/docs/api/workers#open
code: js
const worker = new Worker(new URL("worker.ts", import.meta.url).href);
worker.addEventListener("open", () => {
console.log("worker is ready");
});
Worker が起動されるまでに送られたメッセージはキューに入れられるため、特に待つ必要はない。
Worker の preload オプション
Bun v1.1.35 introduces a preload option for Worker, which allows you to evaluate a script before the worker script is executed.
https://bun.sh/blog/bun-v1.1.35#preload-option-for-worker
code: js
const worker = new Worker(new URL("worker.js", import.meta.url).href, {
preload: new URL("preload.js", import.meta.url).href
});
Worker を作る前にあらかじめ実行するコードを指定できるらしい。
Text Modules
WHATWG で Import Attributes を使った type: "text" の提案がなされている。エンコーディングをどう指定するかなどの議論が起きており、まだ仕様として定まっていない。
https://github.com/whatwg/html/issues/9444
Bun は結論を待たず、すでに独自実装している。
code: js
import html from "./index.html" with { type: "text" };
console.log(html);
https://bun.sh/blog/bun-v1.1.5#new-import-any-file-as-text
Node.js
package.json で JSONC 形式の対応
突然 package.json で JSONC 形式の対応を表明したため賛否両論が巻き起こった。
@jarredsumner: In the next version of Bun
Bun won't error when package.json has comments or trailing commas
https://pbs.twimg.com/media/GLPRdS2asAAaMQh.png
PR にネガティブな意見が集まったが、最終的に Jarred 氏が Twitter 上のアンケート結果を根拠として Bun に取り込んだ。
https://bun.sh/blog/bun-v1.1.5#package-json-with-comments-and-trailing-commas
JSONC 形式の場合当然 npm にパブリッシュ出来ない。互換性がないのだから拡張子を jsonc にしてサポートした方がいいのにと個人的には思う。ちなみに pnpm は JSON5 と YAML をサポートしているがもちろん拡張子を分けている。
https://github.com/pnpm/pnpm/pull/1799
JSX
ショートハンド記法
code: jsx
function Div(props: {className: string;}) {
const {className} = props;
// without punning
return <div className={className} />;
// with punning
return <div {className} />;
}
https://bun.sh/docs/runtime/jsx#prop-punning
JSX の仕様は Meta (Facebook) により管理されている。
https://facebook.github.io/jsx/
特に仕様に対して提案するといったアプローチをとることなく拡張し、他ランタイムへ足並みを揃えるよう呼びかけず、急に TypeScript のパーサーへの対応を要求したため議論を呼んだ。リジェクトされた。
In bun's current implementation, we don't support multiple identifiers in one {}, just <div {foo} />. The rationale was to keep the parser changes as simple as possible and minimize edgecases. From a DX perspective, it would be better to support multiple but I think it's still net improvement to ship without supporting it
https://github.com/microsoft/TypeScript/issues/52057#issuecomment-1372709707
その他
TOML Modules
code: js
import data from "./data.toml";
// there's no toml extension, but type makes it read as toml.
import cfg from "./Configfile" with { type: "toml" };
https://bun.sh/guides/runtime/import-toml
https://bun.sh/blog/bun-v1.1.5#new-import-any-file-as-text