Bun の非互換な拡張 API
https://gyazo.com/a71f94cf1c15e71313d8a2c16c59d1ab
Bun は WinterCG からの招待を無視し、標準から外れた拡張やまだプラットフォームで議論中の仕様を利便性のためだけに取り入れている。またエコシステムとして合意の取れていない実装をすることもある。 @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 😀 これら 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); 仕様でモジュールの種類は Abstruct Module Records、Cyclic Module Records そして Source Text Module Records に分類されている(この列挙順の後ろにあるほどサブクラスとなる)。Cyclic Module Records 以降のモジュールタイプはキャッシュされることが明記されており、またそのキャッシュを操作することは出来ない。すなわち Source Text Module Records である JavaScript モジュール(ES Modules)キャッシュ操作が出来ない。TC39 メンバーである ljharb さんからも ECMAScript にそのような仕様はないと教えてもらった。
@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. つまり Bun は JavaScript 仕様違反をしていると言っても過言ではなさそう。
ところで CommonJS によって読み込んだモジュールは require.cache でキャッシュを触ることが出来るが、これは Abstruct Module Records であると解釈することで一応 ECMAScript 仕様違反ではないと理解することができる。この辺りは uhyo さんのスライドが詳しい。
Web 標準 API
AsyncIterable な console で標準入力から一行ずつ読む
In Bun, the console object can be used as an AsyncIterable to sequentially read lines from process.stdin.
code: js
for await (const line of console) {
console.log(line);
}
もちろんこの機能は Console Standard にはない。
Headers の getAll と toJSON メソッド
Set-Cookie HTTP フィールド(ヘッダー)対応のために Fetch Standard の Headers に getAll メソッドを追加しようという議論が起きていた。最終的に getSetCookie メソッドを追加することになった。
しかし 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.
code: js
const headers = new Headers();
headers.append("Set-Cookie", "a=1");
headers.append("Set-Cookie", "b=1; Secure");
console.log(headers.toJSON()); // { "set-cookie": "a=1, b=1; Secure" }
各ランタイムについての流れは Zenn に書いてある。
Response のコンストラクタに AsyncIterable を渡す
code: js
const response = new Response(async function* () {
yield "hello";
yield "world";
}());
await response.text(); // "helloworld"
これは Fetch Standard に沿っていない。ただし Node.js (undici) でも同じように実行できてしまう問題がある。
Web 標準では ReadableStream.from を経由することで対処できる。ただしこの辺りはまだ議論中(特にプリミティブである string に対してどうするか)で実装しているランタイムは少ない。
code: js
const response = new Response(ReadableStream.from(async function* () {
yield "hello";
yield "world";
}()));
await response.text(); // "helloworld"
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
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.
code: js
// The URL of the proxy server
});
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.)
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.
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" の提案がなされている。エンコーディングをどう指定するかなどの議論が起きており、まだ仕様として定まっていない。 Bun は結論を待たず、すでに独自実装している。
code: js
import html from "./index.html" with { type: "text" };
console.log(html);
Node.js
package.json で JSONC 形式の対応
突然 package.json で JSONC 形式の対応を表明したため賛否両論が巻き起こった。
Bun won't error when package.json has comments or trailing commas
https://pbs.twimg.com/media/GLPRdS2asAAaMQh.png
PR にネガティブな意見が集まったが、最終的に Jarred 氏が Twitter 上のアンケート結果を根拠として Bun に取り込んだ。
JSONC 形式の場合当然 npm にパブリッシュ出来ない。互換性がないのだから拡張子を jsonc にしてサポートした方がいいのにと個人的には思う。ちなみに pnpm で JSON5 と YAML をサポートしているがもちろん拡張子を分けている。
JSX
ショートハンド記法
code: jsx
function Div(props: {className: string;}) {
const {className} = props;
// without punning
return <div className={className} />;
// with punning
return <div {className} />;
}
JSX の仕様は Meta (Facebook) により管理されている。
特に仕様に対して提案するといったアプローチをとることなく拡張し、他ランタイムへ足並みを揃えるよう呼びかけず、急に 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
その他
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" };