aboutsummaryrefslogtreecommitdiff
path: root/test/js/web
diff options
context:
space:
mode:
Diffstat (limited to 'test/js/web')
-rw-r--r--test/js/web/abort/abort-signal-timeout.test.js12
-rw-r--r--test/js/web/console/console-log.expected.txt46
-rw-r--r--test/js/web/console/console-log.js54
-rw-r--r--test/js/web/console/console-log.test.ts20
-rw-r--r--test/js/web/crypto/web-crypto.test.ts91
-rw-r--r--test/js/web/encoding/text-decoder.test.js243
-rw-r--r--test/js/web/encoding/text-encoder.test.js281
-rw-r--r--test/js/web/encoding/utf8-encoding-fixture.binbin0 -> 4456448 bytes
-rw-r--r--test/js/web/fetch/body-mixin-errors.test.ts17
-rw-r--r--test/js/web/fetch/body-stream.test.ts451
-rw-r--r--test/js/web/fetch/fetch-gzip.test.ts181
-rw-r--r--test/js/web/fetch/fetch.js.txt46
-rw-r--r--test/js/web/fetch/fetch.test.ts935
-rw-r--r--test/js/web/fetch/fetch_headers.test.js66
-rw-r--r--test/js/web/fetch/fixture.html1428
-rw-r--r--test/js/web/fetch/fixture.html.gzbin0 -> 16139 bytes
-rw-r--r--test/js/web/html/FormData.test.ts410
-rw-r--r--test/js/web/html/form-data-fixture.txt1
-rw-r--r--test/js/web/streams/bun-streams-test-fifo.sh8
-rw-r--r--test/js/web/streams/fetch.js.txt46
-rw-r--r--test/js/web/streams/streams.test.js630
-rw-r--r--test/js/web/timers/microtask.test.js74
-rw-r--r--test/js/web/timers/performance.test.js22
-rw-r--r--test/js/web/timers/setImmediate.test.js47
-rw-r--r--test/js/web/timers/setInterval.test.js61
-rw-r--r--test/js/web/timers/setTimeout.test.js173
-rw-r--r--test/js/web/url/url.test.ts137
-rw-r--r--test/js/web/util/atob.test.js77
-rw-r--r--test/js/web/web-globals.test.js156
-rw-r--r--test/js/web/websocket/websocket-subprocess.ts13
-rw-r--r--test/js/web/websocket/websocket.test.js263
31 files changed, 5989 insertions, 0 deletions
diff --git a/test/js/web/abort/abort-signal-timeout.test.js b/test/js/web/abort/abort-signal-timeout.test.js
new file mode 100644
index 000000000..7d741b2ad
--- /dev/null
+++ b/test/js/web/abort/abort-signal-timeout.test.js
@@ -0,0 +1,12 @@
+import { expect, test } from "bun:test";
+
+test.skip("AbortSignal.timeout", done => {
+ const abort = AbortSignal.timeout(10);
+ abort.addEventListener("abort", event => {
+ done();
+ });
+
+ // AbortSignal.timeout doesn't keep the event loop / process alive
+ // so we set a no-op timeout
+ setTimeout(() => {}, 11);
+});
diff --git a/test/js/web/console/console-log.expected.txt b/test/js/web/console/console-log.expected.txt
new file mode 100644
index 000000000..97191c8be
--- /dev/null
+++ b/test/js/web/console/console-log.expected.txt
@@ -0,0 +1,46 @@
+Hello World!
+123
+-123
+123.567
+-123.567
+true
+false
+null
+undefined
+Symbol(Symbol Description)
+2000-06-27T02:24:34.304Z
+[ 123, 456, 789 ]
+{
+ name: "foo"
+}
+{
+ a: 123,
+ b: 456,
+ c: 789
+}
+{
+ a: {
+ b: {
+ c: 123
+ },
+ bacon: true
+ },
+ name: "bar"
+}
+Promise { <pending> }
+[Function]
+[Function: Foo]
+{}
+[Function: foooo]
+/FooRegex/
+Is it a bug or a feature that formatting numbers like 123 is colored
+String 123 should be 2nd word, 456 == 456 and percent s %s == What okay
+{
+ foo: {
+ name: "baz"
+ },
+ bar: [Circular]
+} am
+[
+ {}, {}, {}, {}
+]
diff --git a/test/js/web/console/console-log.js b/test/js/web/console/console-log.js
new file mode 100644
index 000000000..e23a3e9cb
--- /dev/null
+++ b/test/js/web/console/console-log.js
@@ -0,0 +1,54 @@
+console.log("Hello World!");
+console.log(123);
+console.log(-123);
+console.log(123.567);
+console.log(-123.567);
+console.log(true);
+console.log(false);
+console.log(null);
+console.log(undefined);
+console.log(Symbol("Symbol Description"));
+console.log(new Date(Math.pow(2, 34) * 56));
+console.log([123, 456, 789]);
+console.log({ name: "foo" });
+console.log({ a: 123, b: 456, c: 789 });
+console.log({
+ a: {
+ b: {
+ c: 123,
+ },
+ bacon: true,
+ },
+ name: "bar",
+});
+
+console.log(new Promise(() => {}));
+
+class Foo {}
+
+console.log(() => {});
+console.log(Foo);
+console.log(new Foo());
+console.log(function foooo() {});
+
+console.log(/FooRegex/);
+
+console.error("uh oh");
+console.time("Check");
+
+console.log("Is it a bug or a feature that formatting numbers like %d is colored", 123);
+//console.log(globalThis);
+
+console.log("String %s should be 2nd word, 456 == %s and percent s %s == %s", "123", "456", "%s", "What", "okay");
+
+const infinteLoop = {
+ foo: {
+ name: "baz",
+ },
+ bar: {},
+};
+
+infinteLoop.bar = infinteLoop;
+console.log(infinteLoop, "am");
+
+console.log(new Array(4).fill({}));
diff --git a/test/js/web/console/console-log.test.ts b/test/js/web/console/console-log.test.ts
new file mode 100644
index 000000000..98c8370de
--- /dev/null
+++ b/test/js/web/console/console-log.test.ts
@@ -0,0 +1,20 @@
+import { file, spawn } from "bun";
+import { expect, it } from "bun:test";
+import { bunExe } from "harness";
+
+it("should log to console correctly", async () => {
+ const { stdout, stderr, exited } = spawn({
+ cmd: [bunExe(), import.meta.dir + "/console-log.js"],
+ stdin: null,
+ stdout: "pipe",
+ stderr: "pipe",
+ env: {
+ BUN_DEBUG_QUIET_LOGS: "1",
+ },
+ });
+ expect(await exited).toBe(0);
+ expect(await new Response(stderr).text()).toBe("uh oh\n");
+ expect(await new Response(stdout).text()).toBe(
+ await new Response(file(import.meta.dir + "/console-log.expected.txt")).text(),
+ );
+});
diff --git a/test/js/web/crypto/web-crypto.test.ts b/test/js/web/crypto/web-crypto.test.ts
new file mode 100644
index 000000000..250282b96
--- /dev/null
+++ b/test/js/web/crypto/web-crypto.test.ts
@@ -0,0 +1,91 @@
+import { describe, expect, it } from "bun:test";
+
+describe("Web Crypto", () => {
+ it("has globals", () => {
+ expect(crypto.subtle !== undefined).toBe(true);
+ expect(CryptoKey.name).toBe("CryptoKey");
+ expect(SubtleCrypto.name).toBe("SubtleCrypto");
+ });
+ it("should encrypt and decrypt", async () => {
+ const key = await crypto.subtle.generateKey(
+ {
+ name: "AES-GCM",
+ length: 256,
+ },
+ true,
+ ["encrypt", "decrypt"],
+ );
+ const iv = crypto.getRandomValues(new Uint8Array(12));
+ const data = new TextEncoder().encode("Hello World!");
+ const encrypted = await crypto.subtle.encrypt(
+ {
+ name: "AES-GCM",
+ iv,
+ },
+ key,
+ data,
+ );
+ const decrypted = await crypto.subtle.decrypt(
+ {
+ name: "AES-GCM",
+ iv,
+ },
+ key,
+ encrypted,
+ );
+ expect(new TextDecoder().decode(decrypted)).toBe("Hello World!");
+ });
+
+ it("should verify and sign", async () => {
+ async function importKey(secret) {
+ return await crypto.subtle.importKey(
+ "raw",
+ new TextEncoder().encode(secret),
+ { name: "HMAC", hash: "SHA-256" },
+ false,
+ ["sign", "verify"],
+ );
+ }
+
+ async function signResponse(message, secret) {
+ const key = await importKey(secret);
+ const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
+
+ // Convert ArrayBuffer to Base64
+ return btoa(String.fromCharCode(...new Uint8Array(signature)));
+ }
+
+ async function verifySignature(message, signature, secret) {
+ const key = await importKey(secret);
+
+ // Convert Base64 to Uint8Array
+ const sigBuf = Uint8Array.from(atob(signature), c => c.charCodeAt(0));
+
+ return await crypto.subtle.verify("HMAC", key, sigBuf, new TextEncoder().encode(message));
+ }
+
+ const msg = `hello world`;
+ const SECRET = "secret";
+ const signature = await signResponse(msg, SECRET);
+
+ const isSigValid = await verifySignature(msg, signature, SECRET);
+ expect(isSigValid).toBe(true);
+ });
+});
+
+describe("Ed25519", () => {
+ describe("generateKey", () => {
+ it("should return CryptoKeys without namedCurve in algorithm field", async () => {
+ const { publicKey, privateKey } = (await crypto.subtle.generateKey("Ed25519", true, [
+ "sign",
+ "verify",
+ ])) as CryptoKeyPair;
+ expect(publicKey.algorithm!.name).toBe("Ed25519");
+ // @ts-ignore
+ expect(publicKey.algorithm!.namedCurve).toBe(undefined);
+ expect(privateKey.algorithm!.name).toBe("Ed25519");
+ // @ts-ignore
+ expect(privateKey.algorithm!.namedCurve).toBe(undefined);
+ });
+ });
+});
diff --git a/test/js/web/encoding/text-decoder.test.js b/test/js/web/encoding/text-decoder.test.js
new file mode 100644
index 000000000..abd4c2a72
--- /dev/null
+++ b/test/js/web/encoding/text-decoder.test.js
@@ -0,0 +1,243 @@
+import { expect, it, describe } from "bun:test";
+import { gc as gcTrace, withoutAggressiveGC } from "harness";
+
+const getByteLength = str => {
+ // returns the byte length of an utf8 string
+ var s = str.length;
+ for (var i = str.length - 1; i >= 0; i--) {
+ var code = str.charCodeAt(i);
+ if (code > 0x7f && code <= 0x7ff) s++;
+ else if (code > 0x7ff && code <= 0xffff) s += 2;
+ if (code >= 0xdc00 && code <= 0xdfff) i--; //trail surrogate
+ }
+ return s;
+};
+
+describe("TextDecoder", () => {
+ it("should not crash on empty text", () => {
+ const decoder = new TextDecoder();
+ gcTrace(true);
+ const fixtures = [new Uint8Array(), new Uint8Array([]), new Buffer(0), new ArrayBuffer(0), new Uint16Array(0)];
+
+ for (let input of fixtures) {
+ expect(decoder.decode(input)).toBe("");
+ }
+
+ // Cause a de-opt
+ try {
+ decoder.decode([NaN, Symbol("s")]);
+ } catch (e) {}
+
+ // DOMJIT test
+ for (let i = 0; i < 90000; i++) {
+ decoder.decode(fixtures[0]);
+ }
+
+ gcTrace(true);
+ });
+ it("should decode ascii text", () => {
+ const decoder = new TextDecoder("latin1");
+ gcTrace(true);
+ expect(decoder.encoding).toBe("windows-1252");
+ gcTrace(true);
+ expect(decoder.decode(new Uint8Array([0x41, 0x42, 0x43]))).toBe("ABC");
+ gcTrace(true);
+
+ // hit the SIMD code path
+ const result = [
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33,
+ ];
+ gcTrace(true);
+ expect(decoder.decode(Uint8Array.from(result))).toBe(String.fromCharCode(...result));
+ gcTrace(true);
+ });
+
+ it("should decode unicode text", () => {
+ const decoder = new TextDecoder();
+ gcTrace(true);
+ const inputBytes = [226, 157, 164, 239, 184, 143, 32, 82, 101, 100, 32, 72, 101, 97, 114, 116];
+ for (var repeat = 1; repeat < 100; repeat++) {
+ var text = `❤️ Red Heart`.repeat(repeat);
+
+ var bytes = Array.from({ length: repeat }, () => inputBytes).flat();
+ var decoded = decoder.decode(Uint8Array.from(bytes));
+ expect(decoder.encoding).toBe("utf-8");
+ expect(decoded).toBe(text);
+ gcTrace(true);
+ }
+ });
+
+ describe("typedArrays", () => {
+ var text = `ABC DEF GHI JKL MNO PQR STU VWX YZ ABC DEF GHI JKL MNO PQR STU V`;
+ var bytes = new TextEncoder().encode(text);
+ var decoder = new TextDecoder();
+ for (let TypedArray of [
+ Uint8Array,
+ Uint16Array,
+ Uint32Array,
+ Int8Array,
+ Int16Array,
+ Int32Array,
+ Float32Array,
+ Float64Array,
+ DataView,
+ BigInt64Array,
+ BigUint64Array,
+ ]) {
+ it(`should decode ${TypedArray.name}`, () => {
+ const decoded = decoder.decode(new TypedArray(bytes.buffer));
+ expect(decoded).toBe(text);
+ });
+ }
+
+ it("DOMJIT call", () => {
+ const array = new Uint8Array(bytes.buffer);
+ withoutAggressiveGC(() => {
+ for (let i = 0; i < 100_000; i++) {
+ const decoded = decoder.decode(array);
+ expect(decoded).toBe(text);
+ }
+ });
+ });
+ });
+
+ it("should decode unicode text with multiple consecutive emoji", () => {
+ const decoder = new TextDecoder();
+ const encoder = new TextEncoder();
+ gcTrace(true);
+ var text = `❤️❤️❤️❤️❤️❤️ Red Heart`;
+
+ text += ` ✨ Sparkles 🔥 Fire 😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 🥺 😢 😭 😤 😠 😡 🤬 🤯 😳 🥵 🥶 😱 😨 😰`;
+ gcTrace(true);
+ expect(decoder.decode(encoder.encode(text))).toBe(text);
+ gcTrace(true);
+ const bytes = new Uint8Array(getByteLength(text) * 8);
+ gcTrace(true);
+ const amount = encoder.encodeInto(text, bytes);
+ gcTrace(true);
+ expect(decoder.decode(bytes.subarray(0, amount.written))).toBe(text);
+ gcTrace(true);
+ });
+});
+
+it("truncated sequences", () => {
+ const assert_equals = (a, b) => expect(a).toBe(b);
+
+ // Truncated sequences
+ assert_equals(new TextDecoder().decode(new Uint8Array([0xf0])), "\uFFFD");
+ assert_equals(new TextDecoder().decode(new Uint8Array([0xf0, 0x9f])), "\uFFFD");
+ assert_equals(new TextDecoder().decode(new Uint8Array([0xf0, 0x9f, 0x92])), "\uFFFD");
+
+ // Errors near end-of-queue
+ assert_equals(new TextDecoder().decode(new Uint8Array([0xf0, 0x9f, 0x41])), "\uFFFDA");
+ assert_equals(new TextDecoder().decode(new Uint8Array([0xf0, 0x41, 0x42])), "\uFFFDAB");
+ assert_equals(new TextDecoder().decode(new Uint8Array([0xf0, 0x41, 0xf0])), "\uFFFDA\uFFFD");
+ assert_equals(new TextDecoder().decode(new Uint8Array([0xf0, 0x8f, 0x92])), "\uFFFD\uFFFD\uFFFD");
+});
diff --git a/test/js/web/encoding/text-encoder.test.js b/test/js/web/encoding/text-encoder.test.js
new file mode 100644
index 000000000..3d271026d
--- /dev/null
+++ b/test/js/web/encoding/text-encoder.test.js
@@ -0,0 +1,281 @@
+import { expect, it, describe } from "bun:test";
+import { gc as gcTrace, withoutAggressiveGC } from "harness";
+
+const getByteLength = str => {
+ // returns the byte length of an utf8 string
+ var s = str.length;
+ for (var i = str.length - 1; i >= 0; i--) {
+ var code = str.charCodeAt(i);
+ if (code > 0x7f && code <= 0x7ff) s++;
+ else if (code > 0x7ff && code <= 0xffff) s += 2;
+ if (code >= 0xdc00 && code <= 0xdfff) i--; //trail surrogate
+ }
+ return s;
+};
+
+describe("TextEncoder", () => {
+ it("should encode latin1 text with non-ascii latin1 characters", () => {
+ var text = "H©ell©o Wor©ld!";
+
+ gcTrace(true);
+ const encoder = new TextEncoder();
+ const encoded = encoder.encode(text);
+ gcTrace(true);
+ const into = new Uint8Array(100);
+ const out = encoder.encodeInto(text, into);
+ gcTrace(true);
+ expect(out.read).toBe(text.length);
+
+ expect(encoded instanceof Uint8Array).toBe(true);
+ const result = [72, 194, 169, 101, 108, 108, 194, 169, 111, 32, 87, 111, 114, 194, 169, 108, 100, 33];
+ for (let i = 0; i < result.length; i++) {
+ expect(encoded[i]).toBe(result[i]);
+ expect(into[i]).toBe(result[i]);
+ }
+ expect(encoded.length).toBe(result.length);
+ expect(out.written).toBe(result.length);
+
+ const repeatCOunt = 16;
+ text = "H©ell©o Wor©ld!".repeat(repeatCOunt);
+ const byteLength = getByteLength(text);
+ const encoded2 = encoder.encode(text);
+ expect(encoded2.length).toBe(byteLength);
+ const into2 = new Uint8Array(byteLength);
+ const out2 = encoder.encodeInto(text, into2);
+ expect(out2.read).toBe(text.length);
+ expect(out2.written).toBe(byteLength);
+ expect(into2).toEqual(encoded2);
+ const repeatedResult = new Uint8Array(byteLength);
+ for (let i = 0; i < repeatCOunt; i++) {
+ repeatedResult.set(result, i * result.length);
+ }
+ expect(into2).toEqual(repeatedResult);
+ });
+
+ it("should encode latin1 text", async () => {
+ gcTrace(true);
+ const text = "Hello World!";
+ const encoder = new TextEncoder();
+ gcTrace(true);
+ const encoded = encoder.encode(text);
+ gcTrace(true);
+ expect(encoded instanceof Uint8Array).toBe(true);
+ expect(encoded.length).toBe(text.length);
+ gcTrace(true);
+ const result = [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33];
+ for (let i = 0; i < result.length; i++) {
+ expect(encoded[i]).toBe(result[i]);
+ }
+
+ let t = [
+ {
+ str: "\u009c\u0097",
+ expected: [194, 156, 194, 151],
+ },
+ {
+ str: "世",
+ expected: [228, 184, 150],
+ },
+ // Less than 0, out of range.
+ {
+ str: -1,
+ expected: [45, 49],
+ },
+ // Greater than 0x10FFFF, out of range.
+ {
+ str: 0x110000,
+ expected: [49, 49, 49, 52, 49, 49, 50],
+ },
+ // The Unicode replacement character.
+ {
+ str: "\uFFFD",
+ expected: [239, 191, 189],
+ },
+ ];
+ for (let { str, expected } of t) {
+ let utf8 = new TextEncoder().encode(str);
+ expect([...utf8]).toEqual(expected);
+ }
+
+ expect([...new TextEncoder().encode(String.fromCodePoint(0))]).toEqual([0]);
+
+ const fixture = new Uint8Array(await Bun.file(import.meta.dir + "/utf8-encoding-fixture.bin").arrayBuffer());
+ const length = 0x110000;
+ let textEncoder = new TextEncoder();
+ let textDecoder = new TextDecoder();
+ let encodeOut = new Uint8Array(length * 4);
+ let encodeIntoOut = new Uint8Array(length * 4);
+ let encodeIntoBuffer = new Uint8Array(4);
+ let encodeDecodedOut = new Uint8Array(length * 4);
+ for (let i = 0, offset = 0; i < length; i++, offset += 4) {
+ const s = String.fromCodePoint(i);
+ const u = textEncoder.encode(s);
+ encodeOut.set(u, offset);
+
+ textEncoder.encodeInto(s, encodeIntoBuffer);
+ encodeIntoOut.set(encodeIntoBuffer, offset);
+
+ const decoded = textDecoder.decode(encodeIntoBuffer);
+ const encoded = textEncoder.encode(decoded);
+ encodeDecodedOut.set(encoded, offset);
+ }
+
+ expect(encodeOut).toEqual(fixture);
+ expect(encodeIntoOut).toEqual(fixture);
+ expect(encodeOut).toEqual(encodeIntoOut);
+ expect(encodeDecodedOut).toEqual(encodeOut);
+ expect(encodeDecodedOut).toEqual(encodeIntoOut);
+ expect(encodeDecodedOut).toEqual(fixture);
+
+ expect(() => textEncoder.encode(String.fromCodePoint(length + 1))).toThrow();
+ });
+
+ it("should encode long latin1 text", async () => {
+ const text = "Hello World!".repeat(1000);
+ const encoder = new TextEncoder();
+ gcTrace(true);
+ const encoded = encoder.encode(text);
+ gcTrace(true);
+ expect(encoded instanceof Uint8Array).toBe(true);
+ expect(encoded.length).toBe(text.length);
+ gcTrace(true);
+ const decoded = new TextDecoder().decode(encoded);
+ expect(decoded).toBe(text);
+ gcTrace();
+ await new Promise(resolve => setTimeout(resolve, 1));
+ gcTrace();
+ expect(decoded).toBe(text);
+ });
+
+ it("should encode latin1 rope text", () => {
+ var text = "Hello";
+ text += " ";
+ text += "World!";
+
+ gcTrace(true);
+ const encoder = new TextEncoder();
+ const encoded = encoder.encode(text);
+ gcTrace(true);
+ const into = new Uint8Array(100);
+ const out = encoder.encodeInto(text, into);
+ gcTrace(true);
+ expect(out.read).toBe(text.length);
+ expect(out.written).toBe(encoded.length);
+ expect(encoded instanceof Uint8Array).toBe(true);
+ const result = [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33];
+ for (let i = 0; i < result.length; i++) {
+ expect(encoded[i]).toBe(result[i]);
+ expect(encoded[i]).toBe(into[i]);
+ }
+ expect(encoded.length).toBe(getByteLength(text));
+ });
+
+ it("should encode latin1 rope text with non-ascii latin1 characters", () => {
+ var text = "H©ell©o";
+ text += " ";
+ text += "Wor©ld!";
+
+ gcTrace(true);
+ const encoder = new TextEncoder();
+ const encoded = encoder.encode(text);
+ gcTrace(true);
+ const into = new Uint8Array(100);
+ const out = encoder.encodeInto(text, into);
+ gcTrace(true);
+ expect(out.read).toBe(text.length);
+
+ expect(encoded instanceof Uint8Array).toBe(true);
+ const result = [72, 194, 169, 101, 108, 108, 194, 169, 111, 32, 87, 111, 114, 194, 169, 108, 100, 33];
+
+ for (let i = 0; i < result.length; i++) {
+ expect(encoded[i]).toBe(into[i]);
+ expect(encoded[i]).toBe(result[i]);
+ }
+ expect(encoded.length).toBe(result.length);
+ expect(out.written).toBe(encoded.length);
+
+ withoutAggressiveGC(() => {
+ for (let i = 0; i < 10_000; i++) {
+ expect(encoder.encodeInto(text, into)).toEqual(out);
+ }
+ });
+ });
+
+ it("should encode utf-16 text", () => {
+ var text = `❤️ Red Heart
+ ✨ Sparkles
+ 🔥 Fire
+ `;
+ var encoder = new TextEncoder();
+ var decoder = new TextDecoder();
+ gcTrace(true);
+ expect(decoder.decode(encoder.encode(text))).toBe(text);
+ gcTrace(true);
+ });
+
+ // this test is from a web platform test in WebKit
+ describe("should use a unicode replacement character for invalid surrogate pairs", () => {
+ var bad = [
+ {
+ encoding: "utf-16le",
+ input: [0x00, 0xd8],
+ expected: "\uFFFD",
+ name: "lone surrogate lead",
+ },
+ {
+ encoding: "utf-16le",
+ input: [0x00, 0xdc],
+ expected: "\uFFFD",
+ name: "lone surrogate trail",
+ },
+ {
+ encoding: "utf-16le",
+ input: [0x00, 0xd8, 0x00, 0x00],
+ expected: "\uFFFD\u0000",
+ name: "unmatched surrogate lead",
+ },
+ {
+ encoding: "utf-16le",
+ input: [0x00, 0xdc, 0x00, 0x00],
+ expected: "\uFFFD\u0000",
+ name: "unmatched surrogate trail",
+ },
+ {
+ encoding: "utf-16le",
+ input: [0x00, 0xdc, 0x00, 0xd8],
+ expected: "\uFFFD\uFFFD",
+ name: "swapped surrogate pair",
+ },
+ ];
+
+ bad.forEach(function (t) {
+ it(t.encoding + " - " + t.name, () => {
+ gcTrace(true);
+ expect(new TextDecoder(t.encoding).decode(new Uint8Array(t.input))).toBe(t.expected);
+ expect(new TextDecoder(t.encoding).decode(new Uint16Array(new Uint8Array(t.input).buffer))).toBe(t.expected);
+ gcTrace(true);
+ });
+ // test(function () {
+ // assert_throws_js(TypeError, function () {
+ // new TextDecoder(t.encoding, { fatal: true }).decode(
+ // new Uint8Array(t.input)
+ // );
+ // });
+ // }, t.encoding + " - " + t.name + " (fatal flag set)");
+ });
+ });
+
+ it("should encode utf-16 rope text", () => {
+ gcTrace(true);
+ var textReal = `❤️ Red Heart ✨ Sparkles 🔥 Fire`;
+
+ var a = textReal.split("");
+ var text = "";
+ for (let j of a) {
+ text += j;
+ }
+
+ var encoder = new TextEncoder();
+ expect(new TextDecoder().decode(encoder.encode(text))).toBe(textReal);
+ });
+});
diff --git a/test/js/web/encoding/utf8-encoding-fixture.bin b/test/js/web/encoding/utf8-encoding-fixture.bin
new file mode 100644
index 000000000..1f9ecf34f
--- /dev/null
+++ b/test/js/web/encoding/utf8-encoding-fixture.bin
Binary files differ
diff --git a/test/js/web/fetch/body-mixin-errors.test.ts b/test/js/web/fetch/body-mixin-errors.test.ts
new file mode 100644
index 000000000..f57bbc56c
--- /dev/null
+++ b/test/js/web/fetch/body-mixin-errors.test.ts
@@ -0,0 +1,17 @@
+import { it, describe, expect } from "bun:test";
+
+describe("body-mixin-errors", () => {
+ it("should fail when bodyUsed", async () => {
+ var res = new Response("a");
+ expect(res.bodyUsed).toBe(false);
+ await res.text();
+ expect(res.bodyUsed).toBe(true);
+
+ try {
+ await res.text();
+ throw new Error("should not get here");
+ } catch (e: any) {
+ expect(e.message).toBe("Body already used");
+ }
+ });
+});
diff --git a/test/js/web/fetch/body-stream.test.ts b/test/js/web/fetch/body-stream.test.ts
new file mode 100644
index 000000000..1cd932ed9
--- /dev/null
+++ b/test/js/web/fetch/body-stream.test.ts
@@ -0,0 +1,451 @@
+// @ts-nocheck
+import { file, gc, serve, ServeOptions } from "bun";
+import { afterAll, afterEach, describe, expect, it, test } from "bun:test";
+import { readFileSync } from "fs";
+
+var port = 0;
+
+{
+ const BodyMixin = [
+ Request.prototype.arrayBuffer,
+ Request.prototype.blob,
+ Request.prototype.text,
+ Request.prototype.json,
+ ];
+ const useRequestObjectValues = [true, false];
+
+ for (let RequestPrototypeMixin of BodyMixin) {
+ for (let useRequestObject of useRequestObjectValues) {
+ describe(`Request.prototoype.${RequestPrototypeMixin.name}() ${
+ useRequestObject ? "fetch(req)" : "fetch(url)"
+ }`, () => {
+ const inputFixture = [
+ [JSON.stringify("Hello World"), JSON.stringify("Hello World")],
+ [JSON.stringify("Hello World 123"), Buffer.from(JSON.stringify("Hello World 123")).buffer],
+ [JSON.stringify("Hello World 456"), Buffer.from(JSON.stringify("Hello World 456"))],
+ [
+ JSON.stringify("EXTREMELY LONG VERY LONG STRING WOW SO LONG YOU WONT BELIEVE IT! ".repeat(100)),
+ Buffer.from(
+ JSON.stringify("EXTREMELY LONG VERY LONG STRING WOW SO LONG YOU WONT BELIEVE IT! ".repeat(100)),
+ ),
+ ],
+ [
+ JSON.stringify("EXTREMELY LONG 🔥 UTF16 🔥 VERY LONG STRING WOW SO LONG YOU WONT BELIEVE IT! ".repeat(100)),
+ Buffer.from(
+ JSON.stringify(
+ "EXTREMELY LONG 🔥 UTF16 🔥 VERY LONG STRING WOW SO LONG YOU WONT BELIEVE IT! ".repeat(100),
+ ),
+ ),
+ ],
+ ];
+
+ for (const [name, input] of inputFixture) {
+ test(`${name.slice(0, Math.min(name.length ?? name.byteLength, 64))}`, async () => {
+ await runInServer(
+ {
+ async fetch(req) {
+ var result = await RequestPrototypeMixin.call(req);
+ if (RequestPrototypeMixin === Request.prototype.json) {
+ result = JSON.stringify(result);
+ }
+ if (typeof result === "string") {
+ expect(result.length).toBe(name.length);
+ expect(result).toBe(name);
+ } else if (result && result instanceof Blob) {
+ expect(result.size).toBe(new TextEncoder().encode(name).byteLength);
+ expect(await result.text()).toBe(name);
+ } else {
+ expect(result.byteLength).toBe(Buffer.from(input).byteLength);
+ expect(Bun.SHA1.hash(result, "base64")).toBe(Bun.SHA1.hash(input, "base64"));
+ }
+ return new Response(result, {
+ headers: req.headers,
+ });
+ },
+ },
+ async url => {
+ var response;
+
+ // once, then batch of 5
+
+ if (useRequestObject) {
+ response = await fetch(
+ new Request({
+ body: input,
+ method: "POST",
+ url: url,
+ headers: {
+ "content-type": "text/plain",
+ },
+ }),
+ );
+ } else {
+ response = await fetch(url, {
+ body: input,
+ method: "POST",
+ headers: {
+ "content-type": "text/plain",
+ },
+ });
+ }
+
+ expect(response.status).toBe(200);
+ expect(response.headers.get("content-length")).toBe(String(Buffer.from(input).byteLength));
+ expect(response.headers.get("content-type")).toBe("text/plain");
+ expect(await response.text()).toBe(name);
+
+ var promises = new Array(5);
+ for (let i = 0; i < 5; i++) {
+ if (useRequestObject) {
+ promises[i] = await fetch(
+ new Request({
+ body: input,
+ method: "POST",
+ url: url,
+ headers: {
+ "content-type": "text/plain",
+ "x-counter": i,
+ },
+ }),
+ );
+ } else {
+ promises[i] = await fetch(url, {
+ body: input,
+ method: "POST",
+ headers: {
+ "content-type": "text/plain",
+ "x-counter": i,
+ },
+ });
+ }
+ }
+
+ const results = await Promise.all(promises);
+ for (let i = 0; i < 5; i++) {
+ const response = results[i];
+ expect(response.status).toBe(200);
+ expect(response.headers.get("content-length")).toBe(String(Buffer.from(input).byteLength));
+ expect(response.headers.get("content-type")).toBe("text/plain");
+ expect(response.headers.get("x-counter")).toBe(String(i));
+ expect(await response.text()).toBe(name);
+ }
+ },
+ );
+ });
+ }
+ });
+ }
+ }
+}
+
+var existingServer;
+async function runInServer(opts: ServeOptions, cb: (url: string) => void | Promise<void>) {
+ var server;
+ const handler = {
+ ...opts,
+ port: port++,
+ fetch(req) {
+ try {
+ return opts.fetch(req);
+ } catch (e) {
+ console.error(e.message);
+ console.log(e.stack);
+ throw e;
+ }
+ },
+ error(err) {
+ console.log(err.message);
+ console.log(err.stack);
+ throw err;
+ },
+ };
+
+ if (!existingServer) {
+ existingServer = server = Bun.serve(handler);
+ } else {
+ server = existingServer;
+ server.reload(handler);
+ }
+
+ try {
+ await cb(`http://${server.hostname}:${server.port}`);
+ } catch (e) {
+ throw e;
+ } finally {
+ }
+}
+
+afterAll(() => {
+ existingServer && existingServer.close();
+ existingServer = null;
+});
+
+function fillRepeating(dstBuffer, start, end) {
+ let len = dstBuffer.length,
+ sLen = end - start,
+ p = sLen;
+ while (p < len) {
+ if (p + sLen > len) sLen = len - p;
+ dstBuffer.copyWithin(p, start, sLen);
+ p += sLen;
+ sLen <<= 1;
+ }
+}
+
+function gc() {
+ Bun.gc(true);
+}
+
+describe("reader", function () {
+ try {
+ // - empty
+ // - 1 byte
+ // - less than the InlineBlob limit
+ // - multiple chunks
+ // - backpressure
+ for (let inputLength of [0, 1, 2, 12, 95, 1024, 1024 * 1024, 1024 * 1024 * 2]) {
+ var bytes = new Uint8Array(inputLength);
+ {
+ const chunk = Math.min(bytes.length, 256);
+ for (var i = 0; i < chunk; i++) {
+ bytes[i] = 255 - i;
+ }
+ }
+
+ if (bytes.length > 255) fillRepeating(bytes, 0, bytes.length);
+
+ for (const huge_ of [
+ bytes,
+ bytes.buffer,
+ new DataView(bytes.buffer),
+ new Int8Array(bytes),
+ new Blob([bytes]),
+
+ new Uint16Array(bytes),
+ new Uint32Array(bytes),
+ new Float64Array(bytes),
+
+ new Int16Array(bytes),
+ new Int32Array(bytes),
+ new Float32Array(bytes),
+
+ // make sure we handle subarray() as expected when reading
+ // typed arrays from native code
+ new Int16Array(bytes).subarray(1),
+ new Int16Array(bytes).subarray(0, new Int16Array(bytes).byteLength - 1),
+ new Int32Array(bytes).subarray(1),
+ new Int32Array(bytes).subarray(0, new Int32Array(bytes).byteLength - 1),
+ new Float32Array(bytes).subarray(1),
+ new Float32Array(bytes).subarray(0, new Float32Array(bytes).byteLength - 1),
+ new Int16Array(bytes).subarray(0, 1),
+ new Int32Array(bytes).subarray(0, 1),
+ new Float32Array(bytes).subarray(0, 1),
+ ]) {
+ gc();
+ const thisArray = huge_;
+ it(`works with ${thisArray.constructor.name}(${
+ thisArray.byteLength ?? thisArray.size
+ }:${inputLength}) via req.body.getReader() in chunks`, async () => {
+ var huge = thisArray;
+ var called = false;
+ gc();
+
+ const expectedHash =
+ huge instanceof Blob
+ ? Bun.SHA1.hash(new Uint8Array(await huge.arrayBuffer()), "base64")
+ : Bun.SHA1.hash(huge, "base64");
+ const expectedSize = huge instanceof Blob ? huge.size : huge.byteLength;
+
+ const out = await runInServer(
+ {
+ async fetch(req) {
+ try {
+ expect(req.headers.get("x-custom")).toBe("hello");
+ expect(req.headers.get("content-type")).toBe("text/plain");
+ expect(req.headers.get("user-agent")).toBe(navigator.userAgent);
+
+ gc();
+ expect(req.headers.get("x-custom")).toBe("hello");
+ expect(req.headers.get("content-type")).toBe("text/plain");
+ expect(req.headers.get("user-agent")).toBe(navigator.userAgent);
+
+ var reader = req.body.getReader();
+ called = true;
+ var buffers = [];
+ while (true) {
+ var { done, value } = await reader.read();
+ if (done) break;
+ buffers.push(value);
+ }
+ const out = new Blob(buffers);
+ gc();
+ expect(out.size).toBe(expectedSize);
+ expect(Bun.SHA1.hash(await out.arrayBuffer(), "base64")).toBe(expectedHash);
+ expect(req.headers.get("x-custom")).toBe("hello");
+ expect(req.headers.get("content-type")).toBe("text/plain");
+ expect(req.headers.get("user-agent")).toBe(navigator.userAgent);
+ gc();
+ return new Response(out, {
+ headers: req.headers,
+ });
+ } catch (e) {
+ console.error(e);
+ throw e;
+ }
+ },
+ },
+ async url => {
+ gc();
+ const response = await fetch(url, {
+ body: huge,
+ method: "POST",
+ headers: {
+ "content-type": "text/plain",
+ "x-custom": "hello",
+ "x-typed-array": thisArray.constructor.name,
+ },
+ });
+ huge = undefined;
+ expect(response.status).toBe(200);
+ const response_body = new Uint8Array(await response.arrayBuffer());
+
+ expect(response_body.byteLength).toBe(expectedSize);
+ expect(Bun.SHA1.hash(response_body, "base64")).toBe(expectedHash);
+
+ gc();
+ expect(response.headers.get("content-type")).toBe("text/plain");
+ gc();
+ },
+ );
+ expect(called).toBe(true);
+ gc();
+ return out;
+ });
+
+ for (let isDirectStream of [true, false]) {
+ const positions = ["begin", "end"];
+ const inner = thisArray => {
+ for (let position of positions) {
+ it(`streaming back ${thisArray.constructor.name}(${
+ thisArray.byteLength ?? thisArray.size
+ }:${inputLength}) starting request.body.getReader() at ${position}`, async () => {
+ var huge = thisArray;
+ var called = false;
+ gc();
+
+ const expectedHash =
+ huge instanceof Blob
+ ? Bun.SHA1.hash(new Uint8Array(await huge.arrayBuffer()), "base64")
+ : Bun.SHA1.hash(huge, "base64");
+ const expectedSize = huge instanceof Blob ? huge.size : huge.byteLength;
+
+ const out = await runInServer(
+ {
+ async fetch(req) {
+ try {
+ var reader;
+
+ if (position === "begin") {
+ reader = req.body.getReader();
+ }
+
+ if (position === "end") {
+ await 1;
+ reader = req.body.getReader();
+ }
+
+ expect(req.headers.get("x-custom")).toBe("hello");
+ expect(req.headers.get("content-type")).toBe("text/plain");
+ expect(req.headers.get("user-agent")).toBe(navigator.userAgent);
+
+ gc();
+ expect(req.headers.get("x-custom")).toBe("hello");
+ expect(req.headers.get("content-type")).toBe("text/plain");
+ expect(req.headers.get("user-agent")).toBe(navigator.userAgent);
+
+ const direct = {
+ type: "direct",
+ async pull(controller) {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) {
+ called = true;
+ controller.end();
+
+ return;
+ }
+ controller.write(value);
+ }
+ },
+ };
+
+ const web = {
+ async pull(controller) {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) {
+ called = true;
+ controller.close();
+ return;
+ }
+ controller.enqueue(value);
+ }
+ },
+ };
+
+ return new Response(new ReadableStream(isDirectStream ? direct : web), {
+ headers: req.headers,
+ });
+ } catch (e) {
+ console.error(e);
+ throw e;
+ }
+ },
+ },
+ async url => {
+ gc();
+ const response = await fetch(url, {
+ body: huge,
+ method: "POST",
+ headers: {
+ "content-type": "text/plain",
+ "x-custom": "hello",
+ "x-typed-array": thisArray.constructor.name,
+ },
+ });
+ huge = undefined;
+ expect(response.status).toBe(200);
+ const response_body = new Uint8Array(await response.arrayBuffer());
+
+ expect(response_body.byteLength).toBe(expectedSize);
+ expect(Bun.SHA1.hash(response_body, "base64")).toBe(expectedHash);
+
+ gc();
+ if (!response.headers.has("content-type")) {
+ console.error(Object.fromEntries(response.headers.entries()));
+ }
+
+ expect(response.headers.get("content-type")).toBe("text/plain");
+ gc();
+ },
+ );
+ expect(called).toBe(true);
+ gc();
+ return out;
+ });
+ }
+ };
+
+ if (isDirectStream) {
+ describe(" direct stream", () => inner(thisArray));
+ } else {
+ describe("default stream", () => inner(thisArray));
+ }
+ }
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ throw e;
+ }
+});
diff --git a/test/js/web/fetch/fetch-gzip.test.ts b/test/js/web/fetch/fetch-gzip.test.ts
new file mode 100644
index 000000000..01eedc54a
--- /dev/null
+++ b/test/js/web/fetch/fetch-gzip.test.ts
@@ -0,0 +1,181 @@
+import { concatArrayBuffers } from "bun";
+import { it, describe, expect } from "bun:test";
+import fs from "fs";
+import { gc, gcTick } from "harness";
+
+it("fetch() with a buffered gzip response works (one chunk)", async () => {
+ var server = Bun.serve({
+ port: 6025,
+
+ async fetch(req) {
+ gcTick(true);
+ return new Response(require("fs").readFileSync(import.meta.dir + "/fixture.html.gz"), {
+ headers: {
+ "Content-Encoding": "gzip",
+ "Content-Type": "text/html; charset=utf-8",
+ },
+ });
+ },
+ });
+ gcTick(true);
+
+ const res = await fetch(`http://${server.hostname}:${server.port}`, { verbose: true });
+ gcTick(true);
+ const arrayBuffer = await res.arrayBuffer();
+ const clone = new Buffer(arrayBuffer);
+ gcTick(true);
+ await (async function () {
+ const second = new Buffer(await Bun.file(import.meta.dir + "/fixture.html").arrayBuffer());
+ gcTick(true);
+ expect(second.equals(clone)).toBe(true);
+ })();
+ gcTick(true);
+ server.stop();
+});
+
+it("fetch() with a redirect that returns a buffered gzip response works (one chunk)", async () => {
+ var server = Bun.serve({
+ port: 6020,
+
+ async fetch(req) {
+ if (req.url.endsWith("/redirect"))
+ return new Response(await Bun.file(import.meta.dir + "/fixture.html.gz").arrayBuffer(), {
+ headers: {
+ "Content-Encoding": "gzip",
+ "Content-Type": "text/html; charset=utf-8",
+ },
+ });
+
+ return Response.redirect("/redirect");
+ },
+ });
+
+ const res = await fetch(`http://${server.hostname}:${server.port}/hey`, { verbose: true });
+ const arrayBuffer = await res.arrayBuffer();
+ expect(
+ new Buffer(arrayBuffer).equals(new Buffer(await Bun.file(import.meta.dir + "/fixture.html").arrayBuffer())),
+ ).toBe(true);
+ server.stop();
+});
+
+it("fetch() with a protocol-relative redirect that returns a buffered gzip response works (one chunk)", async () => {
+ const server = Bun.serve({
+ port: 5018,
+
+ async fetch(req, server) {
+ if (req.url.endsWith("/redirect"))
+ return new Response(await Bun.file(import.meta.dir + "/fixture.html.gz").arrayBuffer(), {
+ headers: {
+ "Content-Encoding": "gzip",
+ "Content-Type": "text/html; charset=utf-8",
+ },
+ });
+
+ return Response.redirect(`://${server.hostname}:${server.port}/redirect`);
+ },
+ });
+
+ const res = await fetch(`http://${server.hostname}:${server.port}/hey`, { verbose: true });
+ expect(res.url).toBe(`http://${server.hostname}:${server.port}/redirect`);
+ expect(res.redirected).toBe(true);
+ expect(res.status).toBe(200);
+ const arrayBuffer = await res.arrayBuffer();
+ expect(
+ new Buffer(arrayBuffer).equals(new Buffer(await Bun.file(import.meta.dir + "/fixture.html").arrayBuffer())),
+ ).toBe(true);
+
+ server.stop();
+});
+
+it("fetch() with a gzip response works (one chunk, streamed, with a delay", async () => {
+ var server = Bun.serve({
+ port: 6081,
+
+ fetch(req) {
+ return new Response(
+ new ReadableStream({
+ type: "direct",
+ async pull(controller) {
+ await 2;
+
+ const buffer = await Bun.file(import.meta.dir + "/fixture.html.gz").arrayBuffer();
+ controller.write(buffer);
+ controller.close();
+ },
+ }),
+ {
+ headers: {
+ "Content-Encoding": "gzip",
+ "Content-Type": "text/html; charset=utf-8",
+ "Content-Length": "1",
+ },
+ },
+ );
+ },
+ });
+
+ const res = await fetch(`http://${server.hostname}:${server.port}`, {});
+ const arrayBuffer = await res.arrayBuffer();
+ expect(
+ new Buffer(arrayBuffer).equals(new Buffer(await Bun.file(import.meta.dir + "/fixture.html").arrayBuffer())),
+ ).toBe(true);
+ server.stop();
+});
+
+it("fetch() with a gzip response works (multiple chunks, TCP server", async done => {
+ const compressed = await Bun.file(import.meta.dir + "/fixture.html.gz").arrayBuffer();
+ var socketToClose;
+ const server = Bun.listen({
+ port: 4024,
+ hostname: "0.0.0.0",
+ socket: {
+ async open(socket) {
+ socketToClose = socket;
+
+ var corked: any[] = [];
+ var cork = true;
+ async function write(chunk) {
+ await new Promise<void>((resolve, reject) => {
+ if (cork) {
+ corked.push(chunk);
+ }
+
+ if (!cork && corked.length) {
+ socket.write(corked.join(""));
+ corked.length = 0;
+ }
+
+ if (!cork) {
+ socket.write(chunk);
+ }
+
+ resolve();
+ });
+ }
+ await write("HTTP/1.1 200 OK\r\n");
+ await write("Content-Encoding: gzip\r\n");
+ await write("Content-Type: text/html; charset=utf-8\r\n");
+ await write("Content-Length: " + compressed.byteLength + "\r\n");
+ await write("X-WTF: " + "lol".repeat(1000) + "\r\n");
+ await write("\r\n");
+ for (var i = 100; i < compressed.byteLength; i += 100) {
+ cork = false;
+ await write(compressed.slice(i - 100, i));
+ }
+ await write(compressed.slice(i - 100));
+ socket.flush();
+ },
+ drain(socket) {},
+ },
+ });
+ await 1;
+
+ const res = await fetch(`http://${server.hostname}:${server.port}`, {});
+ const arrayBuffer = await res.arrayBuffer();
+ expect(
+ new Buffer(arrayBuffer).equals(new Buffer(await Bun.file(import.meta.dir + "/fixture.html").arrayBuffer())),
+ ).toBe(true);
+ socketToClose.end();
+ server.stop();
+ done();
+});
diff --git a/test/js/web/fetch/fetch.js.txt b/test/js/web/fetch/fetch.js.txt
new file mode 100644
index 000000000..5a9b52fcf
--- /dev/null
+++ b/test/js/web/fetch/fetch.js.txt
@@ -0,0 +1,46 @@
+<!doctype html>
+<html>
+<head>
+ <title>Example Domain</title>
+
+ <meta charset="utf-8" />
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <style type="text/css">
+ body {
+ background-color: #f0f0f2;
+ margin: 0;
+ padding: 0;
+ font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+
+ }
+ div {
+ width: 600px;
+ margin: 5em auto;
+ padding: 2em;
+ background-color: #fdfdff;
+ border-radius: 0.5em;
+ box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
+ }
+ a:link, a:visited {
+ color: #38488f;
+ text-decoration: none;
+ }
+ @media (max-width: 700px) {
+ div {
+ margin: 0 auto;
+ width: auto;
+ }
+ }
+ </style>
+</head>
+
+<body>
+<div>
+ <h1>Example Domain</h1>
+ <p>This domain is for use in illustrative examples in documents. You may use this
+ domain in literature without prior coordination or asking for permission.</p>
+ <p><a href="https://www.iana.org/domains/example">More information...</a></p>
+</div>
+</body>
+</html>
diff --git a/test/js/web/fetch/fetch.test.ts b/test/js/web/fetch/fetch.test.ts
new file mode 100644
index 000000000..1185dbd55
--- /dev/null
+++ b/test/js/web/fetch/fetch.test.ts
@@ -0,0 +1,935 @@
+import { serve, sleep } from "bun";
+import { afterAll, afterEach, beforeAll, describe, expect, it, beforeEach } from "bun:test";
+import { chmodSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "fs";
+import { mkfifo } from "mkfifo";
+import { tmpdir } from "os";
+import { join } from "path";
+import { gc, withoutAggressiveGC } from "harness";
+
+const tmp_dir = mkdtempSync(join(realpathSync(tmpdir()), "fetch.test"));
+
+const fixture = readFileSync(join(import.meta.dir, "fetch.js.txt"), "utf8");
+
+let server;
+function startServer({ fetch, ...options }) {
+ server = serve({
+ ...options,
+ fetch,
+ port: 0,
+ });
+}
+
+afterEach(() => {
+ server?.stop?.(true);
+});
+
+afterAll(() => {
+ rmSync(tmp_dir, { force: true, recursive: true });
+});
+
+const payload = new Uint8Array(1024 * 1024 * 2);
+crypto.getRandomValues(payload);
+
+describe("AbortSignal", () => {
+ beforeEach(() => {
+ startServer({
+ async fetch(request) {
+ if (request.url.endsWith("/nodelay")) {
+ return new Response("Hello");
+ }
+ if (request.url.endsWith("/stream")) {
+ const reader = request.body.getReader();
+ const body = new ReadableStream({
+ async pull(controller) {
+ if (!reader) controller.close();
+ const { done, value } = await reader.read();
+ // When no more data needs to be consumed, close the stream
+ if (done) {
+ controller.close();
+ return;
+ }
+ // Enqueue the next data chunk into our target stream
+ controller.enqueue(value);
+ },
+ });
+ return new Response(body);
+ }
+ if (request.method.toUpperCase() === "POST") {
+ const body = await request.text();
+ return new Response(body);
+ }
+ await sleep(15);
+ return new Response("Hello");
+ },
+ });
+ });
+ afterEach(() => {
+ server?.stop?.(true);
+ });
+
+ it("AbortError", async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ expect(async () => {
+ async function manualAbort() {
+ await sleep(1);
+ controller.abort();
+ }
+ await Promise.all([
+ fetch(`http://127.0.0.1:${server.port}`, { signal: signal }).then(res => res.text()),
+ manualAbort(),
+ ]);
+ }).toThrow(new DOMException("The operation was aborted."));
+ });
+
+ it("AbortAfterFinish", async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ await fetch(`http://127.0.0.1:${server.port}/nodelay`, { signal: signal }).then(async res =>
+ expect(await res.text()).toBe("Hello"),
+ );
+ controller.abort();
+ });
+
+ it("AbortErrorWithReason", async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ expect(async () => {
+ async function manualAbort() {
+ await sleep(10);
+ controller.abort(new Error("My Reason"));
+ }
+ await Promise.all([
+ fetch(`http://127.0.0.1:${server.port}`, { signal: signal }).then(res => res.text()),
+ manualAbort(),
+ ]);
+ }).toThrow("My Reason");
+ });
+
+ it("AbortErrorEventListener", async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ signal.addEventListener("abort", ev => {
+ const target = ev.currentTarget;
+ expect(target).toBeDefined();
+ expect(target.aborted).toBe(true);
+ expect(target.reason).toBeDefined();
+ expect(target.reason.name).toBe("AbortError");
+ });
+
+ expect(async () => {
+ async function manualAbort() {
+ await sleep(10);
+ controller.abort();
+ }
+ await Promise.all([
+ fetch(`http://127.0.0.1:${server.port}`, { signal: signal }).then(res => res.text()),
+ manualAbort(),
+ ]);
+ }).toThrow(new DOMException("The operation was aborted."));
+ });
+
+ it("AbortErrorWhileUploading", async () => {
+ const controller = new AbortController();
+
+ expect(async () => {
+ await fetch(`http://localhost:${server.port}`, {
+ method: "POST",
+ body: new ReadableStream({
+ pull(event_controller) {
+ event_controller.enqueue(new Uint8Array([1, 2, 3, 4]));
+ //this will abort immediately should abort before connected
+ controller.abort();
+ },
+ }),
+ signal: controller.signal,
+ });
+ }).toThrow(new DOMException("The operation was aborted."));
+ });
+
+ it("TimeoutError", async () => {
+ const signal = AbortSignal.timeout(10);
+
+ try {
+ await fetch(`http://127.0.0.1:${server.port}`, { signal: signal }).then(res => res.text());
+ expect(() => {}).toThrow();
+ } catch (ex: any) {
+ expect(ex.name).toBe("TimeoutError");
+ }
+ });
+
+ it("Request", async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ async function manualAbort() {
+ await sleep(10);
+ controller.abort();
+ }
+
+ try {
+ const request = new Request(`http://127.0.0.1:${server.port}`, { signal });
+ await Promise.all([fetch(request).then(res => res.text()), manualAbort()]);
+ expect(() => {}).toThrow();
+ } catch (ex: any) {
+ expect(ex.name).toBe("AbortError");
+ }
+ });
+});
+
+describe("Headers", () => {
+ it(".toJSON", () => {
+ const headers = new Headers({
+ "content-length": "123",
+ "content-type": "text/plain",
+ "x-another-custom-header": "Hello World",
+ "x-custom-header": "Hello World",
+ });
+ expect(JSON.stringify(headers.toJSON(), null, 2)).toBe(
+ JSON.stringify(Object.fromEntries(headers.entries()), null, 2),
+ );
+ });
+
+ it(".getSetCookie() with object", () => {
+ const headers = new Headers({
+ "content-length": "123",
+ "content-type": "text/plain",
+ "x-another-custom-header": "Hello World",
+ "x-custom-header": "Hello World",
+ "Set-Cookie": "foo=bar; Path=/; HttpOnly",
+ });
+ expect(headers.count).toBe(5);
+ expect(headers.getAll("set-cookie")).toEqual(["foo=bar; Path=/; HttpOnly"]);
+ });
+
+ it(".getSetCookie() with array", () => {
+ const headers = new Headers([
+ ["content-length", "123"],
+ ["content-type", "text/plain"],
+ ["x-another-custom-header", "Hello World"],
+ ["x-custom-header", "Hello World"],
+ ["Set-Cookie", "foo=bar; Path=/; HttpOnly"],
+ ["Set-Cookie", "foo2=bar2; Path=/; HttpOnly"],
+ ]);
+ expect(headers.count).toBe(6);
+ expect(headers.getAll("set-cookie")).toEqual(["foo=bar; Path=/; HttpOnly", "foo2=bar2; Path=/; HttpOnly"]);
+ });
+
+ it("Set-Cookies init", () => {
+ const headers = new Headers([
+ ["Set-Cookie", "foo=bar"],
+ ["Set-Cookie", "bar=baz"],
+ ["X-bun", "abc"],
+ ["X-bun", "def"],
+ ]);
+ const actual = [...headers];
+ expect(actual).toEqual([
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "bar=baz"],
+ ["x-bun", "abc, def"],
+ ]);
+ expect([...headers.values()]).toEqual(["foo=bar", "bar=baz", "abc, def"]);
+ });
+
+ it("Headers append multiple", () => {
+ const headers = new Headers([
+ ["Set-Cookie", "foo=bar"],
+ ["X-bun", "foo"],
+ ]);
+ headers.append("Set-Cookie", "bar=baz");
+ headers.append("x-bun", "bar");
+ const actual = [...headers];
+
+ // we do not preserve the order
+ // which is kind of bad
+ expect(actual).toEqual([
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "bar=baz"],
+ ["x-bun", "foo, bar"],
+ ]);
+ });
+
+ it("append duplicate set cookie key", () => {
+ const headers = new Headers([["Set-Cookie", "foo=bar"]]);
+ headers.append("set-Cookie", "foo=baz");
+ headers.append("Set-cookie", "baz=bar");
+ const actual = [...headers];
+ expect(actual).toEqual([
+ ["set-cookie", "foo=baz"],
+ ["set-cookie", "baz=bar"],
+ ]);
+ });
+
+ it("set duplicate cookie key", () => {
+ const headers = new Headers([["Set-Cookie", "foo=bar"]]);
+ headers.set("set-Cookie", "foo=baz");
+ headers.set("set-cookie", "bar=qat");
+ const actual = [...headers];
+ expect(actual).toEqual([
+ ["set-cookie", "foo=baz"],
+ ["set-cookie", "bar=qat"],
+ ]);
+ });
+});
+
+describe("fetch", () => {
+ const urls = [
+ "https://example.com",
+ "http://example.com",
+ new URL("https://example.com"),
+ new Request({ url: "https://example.com" }),
+ { toString: () => "https://example.com" },
+ ];
+ for (let url of urls) {
+ gc();
+ let name;
+ if (url instanceof URL) {
+ name = "URL: " + url;
+ } else if (url instanceof Request) {
+ name = "Request: " + url.url;
+ } else if (url.hasOwnProperty("toString")) {
+ name = "Object: " + url.toString();
+ } else {
+ name = url;
+ }
+ it(name, async () => {
+ gc();
+ const response = await fetch(url, { verbose: true });
+ gc();
+ const text = await response.text();
+ gc();
+ expect(fixture).toBe(text);
+ });
+ }
+
+ it('redirect: "manual"', async () => {
+ startServer({
+ fetch(req) {
+ return new Response(null, {
+ status: 302,
+ headers: {
+ Location: "https://example.com",
+ },
+ });
+ },
+ });
+ const response = await fetch(`http://${server.hostname}:${server.port}`, {
+ redirect: "manual",
+ });
+ expect(response.status).toBe(302);
+ expect(response.headers.get("location")).toBe("https://example.com");
+ expect(response.redirected).toBe(true);
+ });
+
+ it('redirect: "follow"', async () => {
+ startServer({
+ fetch(req) {
+ return new Response(null, {
+ status: 302,
+ headers: {
+ Location: "https://example.com",
+ },
+ });
+ },
+ });
+ const response = await fetch(`http://${server.hostname}:${server.port}`, {
+ redirect: "follow",
+ });
+ expect(response.status).toBe(200);
+ expect(response.headers.get("location")).toBe(null);
+ expect(response.redirected).toBe(true);
+ });
+
+ it("provide body", async () => {
+ startServer({
+ fetch(req) {
+ return new Response(req.body);
+ },
+ host: "localhost",
+ });
+
+ // POST with body
+ const url = `http://${server.hostname}:${server.port}`;
+ const response = await fetch(url, { method: "POST", body: "buntastic" });
+ expect(response.status).toBe(200);
+ expect(await response.text()).toBe("buntastic");
+ });
+
+ ["GET", "HEAD", "OPTIONS"].forEach(method =>
+ it(`fail on ${method} with body`, async () => {
+ const url = `http://${server.hostname}:${server.port}`;
+ expect(async () => {
+ await fetch(url, { body: "buntastic" });
+ }).toThrow("fetch() request with GET/HEAD/OPTIONS method cannot have body.");
+ }),
+ );
+});
+
+it("simultaneous HTTPS fetch", async () => {
+ const urls = ["https://example.com", "https://www.example.com"];
+ for (let batch = 0; batch < 4; batch++) {
+ const promises = new Array(20);
+ for (let i = 0; i < 20; i++) {
+ promises[i] = fetch(urls[i % 2]);
+ }
+ const result = await Promise.all(promises);
+ expect(result.length).toBe(20);
+ for (let i = 0; i < 20; i++) {
+ expect(result[i].status).toBe(200);
+ expect(await result[i].text()).toBe(fixture);
+ }
+ }
+});
+
+it("website with tlsextname", async () => {
+ // irony
+ await fetch("https://bun.sh", { method: "HEAD" });
+});
+
+function testBlobInterface(blobbyConstructor, hasBlobFn?) {
+ for (let withGC of [false, true]) {
+ for (let jsonObject of [
+ { hello: true },
+ {
+ hello: "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳",
+ },
+ ]) {
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} json${withGC ? " (with gc) " : ""}`, async () => {
+ if (withGC) gc();
+ var response = blobbyConstructor(JSON.stringify(jsonObject));
+ if (withGC) gc();
+ expect(JSON.stringify(await response.json())).toBe(JSON.stringify(jsonObject));
+ if (withGC) gc();
+ });
+
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> json${
+ withGC ? " (with gc) " : ""
+ }`, async () => {
+ if (withGC) gc();
+ var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject)));
+ if (withGC) gc();
+ expect(JSON.stringify(await response.json())).toBe(JSON.stringify(jsonObject));
+ if (withGC) gc();
+ });
+
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> invalid json${
+ withGC ? " (with gc) " : ""
+ }`, async () => {
+ if (withGC) gc();
+ var response = blobbyConstructor(
+ new TextEncoder().encode(JSON.stringify(jsonObject) + " NOW WE ARE INVALID JSON"),
+ );
+ if (withGC) gc();
+ var failed = false;
+ try {
+ await response.json();
+ } catch (e) {
+ failed = true;
+ }
+ expect(failed).toBe(true);
+ if (withGC) gc();
+ });
+
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} text${withGC ? " (with gc) " : ""}`, async () => {
+ if (withGC) gc();
+ var response = blobbyConstructor(JSON.stringify(jsonObject));
+ if (withGC) gc();
+ expect(await response.text()).toBe(JSON.stringify(jsonObject));
+ if (withGC) gc();
+ });
+
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> text${
+ withGC ? " (with gc) " : ""
+ }`, async () => {
+ if (withGC) gc();
+ var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject)));
+ if (withGC) gc();
+ expect(await response.text()).toBe(JSON.stringify(jsonObject));
+ if (withGC) gc();
+ });
+
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer${withGC ? " (with gc) " : ""}`, async () => {
+ if (withGC) gc();
+
+ var response = blobbyConstructor(JSON.stringify(jsonObject));
+ if (withGC) gc();
+
+ const bytes = new TextEncoder().encode(JSON.stringify(jsonObject));
+ if (withGC) gc();
+
+ const compare = new Uint8Array(await response.arrayBuffer());
+ if (withGC) gc();
+
+ withoutAggressiveGC(() => {
+ for (let i = 0; i < compare.length; i++) {
+ if (withGC) gc();
+
+ expect(compare[i]).toBe(bytes[i]);
+ if (withGC) gc();
+ }
+ });
+ if (withGC) gc();
+ });
+
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> arrayBuffer${
+ withGC ? " (with gc) " : ""
+ }`, async () => {
+ if (withGC) gc();
+
+ var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject)));
+ if (withGC) gc();
+
+ const bytes = new TextEncoder().encode(JSON.stringify(jsonObject));
+ if (withGC) gc();
+
+ const compare = new Uint8Array(await response.arrayBuffer());
+ if (withGC) gc();
+
+ withoutAggressiveGC(() => {
+ for (let i = 0; i < compare.length; i++) {
+ if (withGC) gc();
+
+ expect(compare[i]).toBe(bytes[i]);
+ if (withGC) gc();
+ }
+ });
+ if (withGC) gc();
+ });
+
+ hasBlobFn &&
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} blob${withGC ? " (with gc) " : ""}`, async () => {
+ if (withGC) gc();
+ const text = JSON.stringify(jsonObject);
+ var response = blobbyConstructor(text);
+ if (withGC) gc();
+ const size = new TextEncoder().encode(text).byteLength;
+ if (withGC) gc();
+ const blobed = await response.blob();
+ if (withGC) gc();
+ expect(blobed instanceof Blob).toBe(true);
+ if (withGC) gc();
+ expect(blobed.size).toBe(size);
+ if (withGC) gc();
+ blobed.type = "";
+ if (withGC) gc();
+ expect(blobed.type).toBe("");
+ if (withGC) gc();
+ blobed.type = "application/json";
+ if (withGC) gc();
+ expect(blobed.type).toBe("application/json");
+ if (withGC) gc();
+ const out = await blobed.text();
+ expect(out).toBe(text);
+ if (withGC) gc();
+ await new Promise(resolve => setTimeout(resolve, 1));
+ if (withGC) gc();
+ expect(out).toBe(text);
+ const first = await blobed.arrayBuffer();
+ const initial = first[0];
+ first[0] = 254;
+ const second = await blobed.arrayBuffer();
+ expect(second[0]).toBe(initial);
+ expect(first[0]).toBe(254);
+ });
+ }
+ }
+}
+
+describe("Bun.file", () => {
+ let count = 0;
+ testBlobInterface(data => {
+ const blob = new Blob([data]);
+ const buffer = Bun.peek(blob.arrayBuffer());
+ const path = join(tmp_dir, `tmp-${count++}.bytes`);
+ writeFileSync(path, buffer);
+ const file = Bun.file(path);
+ expect(blob.size).toBe(file.size);
+ return file;
+ });
+
+ it("size is Infinity on a fifo", () => {
+ const path = join(tmp_dir, "test-fifo");
+ mkfifo(path);
+ const { size } = Bun.file(path);
+ expect(size).toBe(Infinity);
+ });
+
+ function forEachMethod(fn, skip?) {
+ const method = ["arrayBuffer", "text", "json"];
+ for (const m of method) {
+ (skip ? it.skip : it)(m, fn(m));
+ }
+ }
+
+ describe("bad permissions throws", () => {
+ const path = join(tmp_dir, "my-new-file");
+ beforeAll(async () => {
+ await Bun.write(path, "hey");
+ chmodSync(path, 0o000);
+ });
+
+ forEachMethod(
+ m => () => {
+ const file = Bun.file(path);
+ expect(async () => await file[m]()).toThrow("Permission denied");
+ },
+ () => {
+ try {
+ readFileSync(path);
+ } catch {
+ return false;
+ }
+ return true;
+ },
+ );
+ });
+
+ describe("non-existent file throws", () => {
+ const path = join(tmp_dir, "does-not-exist");
+
+ forEachMethod(m => async () => {
+ const file = Bun.file(path);
+ expect(async () => await file[m]()).toThrow("No such file or directory");
+ });
+ });
+});
+
+describe("Blob", () => {
+ testBlobInterface(data => new Blob([data]));
+
+ var blobConstructorValues = [
+ ["123", "456"],
+ ["123", 456],
+ ["123", "456", "789"],
+ ["123", 456, 789],
+ [1, 2, 3, 4, 5, 6, 7, 8, 9],
+ [Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 9])],
+ [Uint8Array.from([1, 2, 3, 4]), "5678", 9],
+ [new Blob([Uint8Array.from([1, 2, 3, 4])]), "5678", 9],
+ [
+ new Blob([
+ new TextEncoder().encode(
+ "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳",
+ ),
+ ]),
+ ],
+ [
+ new TextEncoder().encode(
+ "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳",
+ ),
+ ],
+ ];
+
+ var expected = [
+ "123456",
+ "123456",
+ "123456789",
+ "123456789",
+ "123456789",
+ "\x01\x02\x03\x04\x05\x06\x07\t",
+ "\x01\x02\x03\x0456789",
+ "\x01\x02\x03\x0456789",
+ "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳",
+ "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳",
+ ];
+
+ it(`blobConstructorValues`, async () => {
+ for (let i = 0; i < blobConstructorValues.length; i++) {
+ var response = new Blob(blobConstructorValues[i]);
+ const res = await response.text();
+ if (res !== expected[i]) {
+ throw new Error(
+ `Failed: ${expected[i].split("").map(a => a.charCodeAt(0))}, received: ${res
+ .split("")
+ .map(a => a.charCodeAt(0))}`,
+ );
+ }
+
+ expect(res).toBe(expected[i]);
+ }
+ });
+
+ for (let withGC of [false, true]) {
+ it(`Blob.slice() ${withGC ? " with gc" : ""}`, async () => {
+ var parts = ["hello", " ", "world"];
+ if (withGC) gc();
+ var str = parts.join("");
+ if (withGC) gc();
+ var combined = new Blob(parts);
+ if (withGC) gc();
+ for (let part of parts) {
+ if (withGC) gc();
+ expect(await combined.slice(str.indexOf(part), str.indexOf(part) + part.length).text()).toBe(part);
+ if (withGC) gc();
+ }
+ if (withGC) gc();
+ for (let part of parts) {
+ if (withGC) gc();
+ expect(await combined.slice(str.indexOf(part), str.indexOf(part) + part.length).text()).toBe(part);
+ if (withGC) gc();
+ }
+ });
+ }
+});
+
+{
+ const sample = new TextEncoder().encode("Hello World!");
+ const typedArrays = [
+ Uint8Array,
+ Uint8ClampedArray,
+ Int8Array,
+ Uint16Array,
+ Int16Array,
+ Uint32Array,
+ Int32Array,
+ Float32Array,
+ Float64Array,
+ ];
+ const Constructors = [Blob, Response, Request];
+
+ for (let withGC of [false, true]) {
+ for (let TypedArray of typedArrays) {
+ for (let Constructor of Constructors) {
+ it(`${Constructor.name} arrayBuffer() with ${TypedArray.name}${withGC ? " with gc" : ""}`, async () => {
+ const data = new TypedArray(sample);
+ if (withGC) gc();
+ const input = Constructor === Blob ? [data] : Constructor === Request ? { body: data } : data;
+ if (withGC) gc();
+ const blob = new Constructor(input);
+ if (withGC) gc();
+ const out = await blob.arrayBuffer();
+ if (withGC) gc();
+ expect(out instanceof ArrayBuffer).toBe(true);
+ if (withGC) gc();
+ expect(out.byteLength).toBe(data.byteLength);
+ if (withGC) gc();
+ });
+ }
+ }
+ }
+}
+
+describe("Response", () => {
+ describe("Response.json", () => {
+ it("works", async () => {
+ const inputs = ["hellooo", [[123], 456, 789], { hello: "world" }, { ok: "😉 😌 😍 🥰 😘 " }];
+ for (let input of inputs) {
+ const output = JSON.stringify(input);
+ expect(await Response.json(input).text()).toBe(output);
+ }
+ // JSON.stringify() returns undefined
+ expect(await Response.json().text()).toBe("");
+ // JSON.stringify("") returns '""'
+ expect(await Response.json("").text()).toBe('""');
+ });
+ it("sets the content-type header", () => {
+ let response = Response.json("hello");
+ expect(response.type).toBe("basic");
+ expect(response.headers.get("content-type")).toBe("application/json;charset=utf-8");
+ expect(response.status).toBe(200);
+ });
+ it("supports number status code", () => {
+ let response = Response.json("hello", 407);
+ expect(response.type).toBe("basic");
+ expect(response.headers.get("content-type")).toBe("application/json;charset=utf-8");
+ expect(response.status).toBe(407);
+ });
+
+ it("supports headers", () => {
+ var response = Response.json("hello", {
+ headers: {
+ "content-type": "potato",
+ "x-hello": "world",
+ },
+ status: 408,
+ });
+
+ expect(response.headers.get("x-hello")).toBe("world");
+ expect(response.status).toBe(408);
+ });
+ });
+ describe("Response.redirect", () => {
+ it("works", () => {
+ const inputs = [
+ "http://example.com",
+ "http://example.com/",
+ "http://example.com/hello",
+ "http://example.com/hello/",
+ "http://example.com/hello/world",
+ "http://example.com/hello/world/",
+ ];
+ for (let input of inputs) {
+ expect(Response.redirect(input).headers.get("Location")).toBe(input);
+ }
+ });
+
+ it("supports headers", () => {
+ var response = Response.redirect("https://example.com", {
+ headers: {
+ "content-type": "potato",
+ "x-hello": "world",
+ Location: "https://wrong.com",
+ },
+ status: 408,
+ });
+ expect(response.headers.get("x-hello")).toBe("world");
+ expect(response.headers.get("Location")).toBe("https://example.com");
+ expect(response.status).toBe(302);
+ expect(response.type).toBe("basic");
+ expect(response.ok).toBe(false);
+ });
+ });
+ describe("Response.error", () => {
+ it("works", () => {
+ expect(Response.error().type).toBe("error");
+ expect(Response.error().ok).toBe(false);
+ expect(Response.error().status).toBe(0);
+ });
+ });
+ it("clone", async () => {
+ gc();
+ var body = new Response("<div>hello</div>", {
+ headers: {
+ "content-type": "text/html; charset=utf-8",
+ },
+ });
+ gc();
+ var clone = body.clone();
+ gc();
+ body.headers.set("content-type", "text/plain");
+ gc();
+ expect(clone.headers.get("content-type")).toBe("text/html; charset=utf-8");
+ gc();
+ expect(body.headers.get("content-type")).toBe("text/plain");
+ gc();
+ expect(await clone.text()).toBe("<div>hello</div>");
+ gc();
+ });
+ it("invalid json", async () => {
+ gc();
+ var body = new Response("<div>hello</div>", {
+ headers: {
+ "content-type": "text/html; charset=utf-8",
+ },
+ });
+ try {
+ await body.json();
+ expect(false).toBe(true);
+ } catch (exception) {
+ expect(exception instanceof SyntaxError).toBe(true);
+ }
+ });
+
+ testBlobInterface(data => new Response(data), true);
+});
+
+describe("Request", () => {
+ it("clone", async () => {
+ gc();
+ var body = new Request("https://hello.com", {
+ headers: {
+ "content-type": "text/html; charset=utf-8",
+ },
+ body: "<div>hello</div>",
+ });
+ gc();
+ expect(body.signal).toBeDefined();
+ gc();
+ expect(body.headers.get("content-type")).toBe("text/html; charset=utf-8");
+ gc();
+ var clone = body.clone();
+ gc();
+ expect(clone.signal).toBeDefined();
+ gc();
+ body.headers.set("content-type", "text/plain");
+ gc();
+ expect(clone.headers.get("content-type")).toBe("text/html; charset=utf-8");
+ gc();
+ expect(body.headers.get("content-type")).toBe("text/plain");
+ gc();
+ expect(await clone.text()).toBe("<div>hello</div>");
+ });
+
+ it("signal", async () => {
+ gc();
+ const controller = new AbortController();
+ const req = new Request("https://hello.com", { signal: controller.signal });
+ expect(req.signal.aborted).toBe(false);
+ gc();
+ controller.abort();
+ gc();
+ expect(req.signal.aborted).toBe(true);
+ });
+
+ it("cloned signal", async () => {
+ gc();
+ const controller = new AbortController();
+ const req = new Request("https://hello.com", { signal: controller.signal });
+ expect(req.signal.aborted).toBe(false);
+ gc();
+ controller.abort();
+ gc();
+ expect(req.signal.aborted).toBe(true);
+ gc();
+ const cloned = req.clone();
+ expect(cloned.signal.aborted).toBe(true);
+ });
+
+ testBlobInterface(data => new Request("https://hello.com", { body: data }), true);
+});
+
+describe("Headers", () => {
+ it("writes", async () => {
+ var headers = new Headers({
+ "content-type": "text/html; charset=utf-8",
+ });
+ gc();
+ expect(headers.get("content-type")).toBe("text/html; charset=utf-8");
+ gc();
+ headers.delete("content-type");
+ gc();
+ expect(headers.get("content-type")).toBe(null);
+ gc();
+ headers.append("content-type", "text/plain");
+ gc();
+ expect(headers.get("content-type")).toBe("text/plain");
+ gc();
+ headers.append("content-type", "text/plain");
+ gc();
+ expect(headers.get("content-type")).toBe("text/plain, text/plain");
+ gc();
+ headers.set("content-type", "text/html; charset=utf-8");
+ gc();
+ expect(headers.get("content-type")).toBe("text/html; charset=utf-8");
+
+ headers.delete("content-type");
+ gc();
+ expect(headers.get("content-type")).toBe(null);
+ gc();
+ });
+});
+
+it("body nullable", async () => {
+ gc();
+ {
+ const req = new Request("https://hello.com", { body: null });
+ expect(req.body).toBeNull();
+ }
+ gc();
+ {
+ const req = new Request("https://hello.com", { body: undefined });
+ expect(req.body).toBeNull();
+ }
+ gc();
+ {
+ const req = new Request("https://hello.com");
+ expect(req.body).toBeNull();
+ }
+ gc();
+ {
+ const req = new Request("https://hello.com", { body: "" });
+ expect(req.body).not.toBeNull();
+ }
+});
diff --git a/test/js/web/fetch/fetch_headers.test.js b/test/js/web/fetch/fetch_headers.test.js
new file mode 100644
index 000000000..cd2786c08
--- /dev/null
+++ b/test/js/web/fetch/fetch_headers.test.js
@@ -0,0 +1,66 @@
+import { describe, it, expect, beforeAll, afterAll } from "bun:test";
+const port = 3009;
+const url = `http://localhost:${port}`;
+let server;
+
+describe("Headers", async () => {
+ // Start up a single server and reuse it between tests
+ beforeAll(() => {
+ server = Bun.serve({
+ fetch(req) {
+ const hdr = req.headers.get("x-test");
+ return new Response(hdr);
+ },
+ port: port,
+ });
+ });
+ afterAll(() => {
+ server.stop();
+ });
+
+ it("Headers should work", async () => {
+ expect(await fetchContent({ "x-test": "header 1" })).toBe("header 1");
+ });
+
+ it("Header names must be valid", async () => {
+ expect(() => fetch(url, { headers: { "a\tb:c": "foo" } })).toThrow("Invalid header name: 'a\tb:c'");
+ expect(() => fetch(url, { headers: { "❤️": "foo" } })).toThrow("Invalid header name: '❤️'");
+ });
+
+ it("Header values must be valid", async () => {
+ expect(() => fetch(url, { headers: { "x-test": "\0" } })).toThrow("Header 'x-test' has invalid value: '\0'");
+ expect(() => fetch(url, { headers: { "x-test": "❤️" } })).toThrow("Header 'x-test' has invalid value: '❤️'");
+ });
+
+ it("repro 1602", async () => {
+ const origString = "😂1234".slice(3);
+
+ var encoder = new TextEncoder();
+ var decoder = new TextDecoder();
+ const roundTripString = decoder.decode(encoder.encode(origString));
+
+ expect(roundTripString).toBe(origString);
+
+ // This one will pass
+ expect(await fetchContent({ "x-test": roundTripString })).toBe(roundTripString);
+ // This would hang
+ expect(await fetchContent({ "x-test": origString })).toBe(origString);
+ });
+
+ describe("toJSON()", () => {
+ it("should provide lowercase header names", () => {
+ const headers1 = new Headers({ "X-Test": "yep", "Content-Type": "application/json" });
+ expect(headers1.toJSON()).toEqual({ "x-test": "yep", "content-type": "application/json" });
+
+ const headers2 = new Headers();
+ headers2.append("X-Test", "yep");
+ headers2.append("Content-Type", "application/json");
+ expect(headers2.toJSON()).toEqual({ "x-test": "yep", "content-type": "application/json" });
+ });
+ });
+});
+
+async function fetchContent(headers) {
+ const res = await fetch(url, { headers: headers }, { verbose: true });
+ return await res.text();
+}
diff --git a/test/js/web/fetch/fixture.html b/test/js/web/fetch/fixture.html
new file mode 100644
index 000000000..081040506
--- /dev/null
+++ b/test/js/web/fetch/fixture.html
@@ -0,0 +1,1428 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta property="og:title" content="Bun is a fast all-in-one JavaScript runtime" />
+ <title>Bun is a fast all-in-one JavaScript runtime</title>
+ <meta
+ property="og:description"
+ content="Bundle, transpile, install and run JavaScript &amp; TypeScript
+ projects – all in Bun. Bun is a new JavaScript runtime with
+ a native bundler, transpiler, task runner and npm client built-in."
+ />
+ <meta name="og:locale" content="en_US" />
+ <meta name="twitter:site" content="@jarredsumner" />
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta property="og:image" content="https://bun.sh/share.png" />
+ <meta
+ name="description"
+ content="Bundle, transpile, install and run JavaScript &amp; TypeScript
+ projects – all in Bun. Bun is a new JavaScript runtime with
+ a native bundler, transpiler, task runner and npm client built-in."
+ />
+ <meta name="theme-color" content="#fbf0df" />
+ <link rel="manifest" href="manifest.json" />
+ <link rel="icon" type="image/png" sizes="256x256" href="/logo-square.png" />
+ <link rel="icon" type="image/png" sizes="32x32" href="/logo-square@32px.png" />
+ <link rel="icon" type="image/png" sizes="16x16" href="/logo-square@16px.png" />
+ <style>
+ :root {
+ --black: #0b0a08;
+ --blue: #00a6e1;
+ --orange: #f89b4b;
+ --orange-light: #d4d3d2;
+ --monospace-font: "Fira Code", "Hack", "Source Code Pro", "SF Mono", "Inconsolata", monospace;
+ --dark-border: rgba(200, 200, 25, 0.2);
+ --max-width: 1152px;
+ --system-font: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
+ "Open Sans", "Helvetica Neue", sans-serif;
+ --horizontal-padding: 3rem;
+ --vertical-padding: 4rem;
+ --line-height: 1.4;
+ }
+ * {
+ box-sizing: border-box;
+ }
+ head,
+ body,
+ :root {
+ margin: 0 auto;
+ padding: 0;
+ font-family: var(--system-font);
+ }
+ body {
+ background-color: #fbf0df;
+ }
+ a {
+ color: inherit;
+ text-decoration: none;
+ transition: transform 0.1s linear;
+ }
+ a:visited {
+ color: inherit;
+ }
+ a:hover {
+ text-decoration: underline;
+ transform: scale(1.06);
+ transform-origin: middle center;
+ }
+ #header-wrap,
+ #pitch {
+ background-color: var(--black);
+ color: #fbf0df;
+ width: 100%;
+ }
+ #logo-link {
+ width: fit-content;
+ display: flex;
+ gap: 24px;
+ align-items: center;
+ }
+ main {
+ width: auto;
+ margin: 0 auto;
+ max-width: var(--max-width);
+ display: grid;
+ grid-template-columns: auto auto;
+ overflow-y: hidden;
+ }
+ main,
+ header,
+ #explain-section {
+ margin: 0 auto;
+ max-width: var(--max-width);
+ padding: 0 var(--horizontal-padding);
+ }
+ #cards-wrap,
+ #usecases,
+ main,
+ header {
+ padding: var(--vertical-padding) var(--horizontal-padding);
+ }
+ #pitch-content {
+ max-width: 600px;
+ }
+ .tagline {
+ margin-top: 0;
+ line-height: 1;
+ font-size: 36pt;
+ }
+ .subtitle {
+ font-size: 1.2rem;
+ }
+ .Navigation ul {
+ white-space: nowrap;
+ display: flex;
+ gap: 2rem;
+ list-style: none;
+ }
+ .NavText {
+ color: #fbf0df;
+ display: block;
+ font-weight: 500;
+ font-size: 1.2rem;
+ }
+ #HeaderInstallButton {
+ margin-left: 2.4rem;
+ }
+ #pitch main {
+ gap: 2rem;
+ }
+ #logo {
+ max-width: 70px;
+ margin: auto 0;
+ }
+ #logo-text {
+ max-width: 96px;
+ }
+ header {
+ display: grid;
+ grid-template-columns: auto max-content;
+ background-color: var(--black);
+ padding: 1.5rem 3rem;
+ align-items: center;
+ color: #fff;
+ }
+ #HeaderInstallButton:hover {
+ cursor: pointer;
+ transform: scale(1.06);
+ }
+ #HeaderInstallButton {
+ transition: transform 0.1s linear;
+ background: #00a6e1;
+ padding: 8px 16px;
+ border-radius: 100px;
+ color: #000;
+ font-weight: 500;
+ }
+ .InstallBox {
+ margin-top: 2rem;
+ background: #15140e;
+ padding: 24px;
+ border-radius: 24px;
+ user-select: none;
+ -webkit-user-select: none;
+ -webkit-user-drag: none;
+ -moz-user-select: none;
+ }
+ .InstallBox-label-heading {
+ font-size: 1.4rem;
+ margin-bottom: 1rem;
+ font-weight: 500;
+ }
+ .InstallBox-label-subtitle {
+ font-size: 0.9rem;
+ color: var(--orange-light);
+ }
+ #usecases-section {
+ background: linear-gradient(12deg, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.2)),
+ conic-gradient(
+ from 6.27deg at 46.95% 50.05%,
+ #ff8181 0deg,
+ #e5f067 75deg,
+ #6dd9ba 155.62deg,
+ #67f0ae 168.75deg,
+ #8b67f0 243.75deg,
+ #f067e2 300deg,
+ #e967e3 334.49deg,
+ #f06767 348.9deg,
+ #ff8181 360deg
+ );
+ color: #fff;
+ font-family: var(--monospace-font);
+ contain: paint;
+ font-size: 24pt;
+ font-weight: 700;
+ }
+ #usecases-section {
+ padding: 0;
+ margin: 0;
+ }
+ #usecases {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+ }
+ #usecases-section h1 {
+ background: linear-gradient(90deg, #ff0000 0%, #faff00 50.52%, #0500ff 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
+ font-family: Helvetica;
+ margin: 0;
+ padding: 0;
+ }
+ .InstallBox-code-box {
+ background-color: #252420;
+ padding: 4px 16px;
+ position: relative;
+ border-radius: 8px;
+ text-align: center;
+ align-items: center;
+ border: 1px solid var(--orange);
+ margin-top: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-content: center;
+ white-space: nowrap;
+ margin-bottom: 1rem;
+ font-family: var(--monospace-font);
+ }
+ .InstallBox-curl {
+ user-select: all;
+ -webkit-user-select: text;
+ pointer-events: auto;
+ white-space: nowrap;
+ cursor: text;
+ display: inline-flex;
+ padding: 12px 8px;
+ gap: 2ch;
+ }
+ .InstallBox-curl:before {
+ display: block;
+ content: "$" / "";
+ color: var(--orange);
+ pointer-events: none;
+ width: 1ch;
+ height: 1ch;
+ }
+ .InstallBox-view-source-link {
+ color: var(--orange-light);
+ }
+ .InstallBox-copy {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ color: var(--orange-light);
+ transition: transform 0.05s linear;
+ transition-property: color, transform;
+ transform-origin: center center;
+ cursor: pointer;
+ background: transparent;
+ border: none;
+ font-size: inherit;
+ font-family: inherit;
+ }
+ .InstallBox-copy:hover {
+ color: var(--blue);
+ transform: scale(1.06);
+ }
+ .InstallBox-copy:active {
+ transform: scale(1.12);
+ }
+ .Tabs {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ margin-left: auto;
+ margin-right: auto;
+ justify-content: center;
+ align-items: center;
+ width: min-content;
+ white-space: nowrap;
+ padding: 0;
+ }
+ .Tab {
+ width: min-content;
+ border: none;
+ background-color: transparent;
+ font-family: var(--monospace-font);
+ text-align: center;
+ border-bottom: 1px solid #ccc;
+ cursor: pointer;
+ padding: 16px;
+ color: inherit;
+ font-size: inherit;
+ }
+ .Tab:hover,
+ .Graphs--active-react .Tab[data-tab="react"],
+ .Graphs--active-sqlite .Tab[data-tab="sqlite"],
+ .Graphs--active-websocket .Tab[data-tab="websocket"] {
+ border-bottom-color: #7fffd4;
+ background-color: #82d8f71a;
+ border-right-color: #7fffd4;
+ border-left-color: #7fffd4;
+ }
+ .BarGraph {
+ padding: 24px;
+ display: flex;
+ flex-direction: column;
+ }
+ .BarGraph-heading {
+ font-weight: 500;
+ font-size: 1.5rem;
+ margin: 0;
+ }
+ .BarGraphList {
+ flex: 1;
+ position: relative;
+ list-style-type: none;
+ padding: 0;
+ }
+ .BarGraph,
+ .ActiveTab,
+ .Graphs {
+ height: auto;
+ }
+ .BarGraph-subheading {
+ font-size: 0.9rem;
+ color: #878686;
+ margin: 0;
+ }
+ .BarGraphList {
+ margin-top: 1rem;
+ display: grid;
+ grid-template-columns: repeat(var(--count), 1fr);
+ font-variant-numeric: tabular-nums;
+ font-family: var(--monospace-font);
+ justify-content: center;
+ align-items: flex-start;
+ height: 100%;
+ background-color: #080808;
+ }
+ .BarGraphKey {
+ display: grid;
+ text-align: center;
+ margin-top: 1rem;
+ grid-template-columns: repeat(var(--count), 1fr);
+ }
+ .BarGraphBar {
+ --primary: 70px;
+ --opposite: 100%;
+ }
+ .BarGraph,
+ .BarGraphBar-label,
+ .BarGraphItem {
+ --level: calc(var(--amount) / var(--max));
+ --inverse: calc(1 / var(--level));
+ }
+ .BarGraphBar {
+ margin: 0 auto;
+ width: var(--primary);
+ height: var(--opposite);
+ background-color: #5d5986;
+ position: relative;
+ height: calc(200px * var(--level));
+ }
+ .BarGraphItem {
+ border-right: 1px dashed var(--dark-border);
+ border-top: 1px dashed var(--dark-border);
+ border-bottom: 1px dashed var(--dark-border);
+ min-height: 200px;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ }
+ .BarGraphItem--deno {
+ border-right-color: transparent;
+ }
+ .BarGraph--vertical .BarGraphBar {
+ max-width: 90%;
+ }
+ .BarGraphBar-label {
+ color: #fff;
+ font-variant-numeric: tabular-nums;
+ font-family: var(--monospace-font);
+ width: 100%;
+ text-align: center;
+ position: relative;
+ display: flex;
+ justify-content: center;
+ }
+ .CardContent {
+ position: relative;
+ }
+ .BarGraph--vertical .BarGraphBar-label {
+ transform: scaleX(var(--inverse));
+ bottom: 0;
+ right: 0;
+ }
+ .BarGraph--horizontal .BarGraphBar-label {
+ top: -22px;
+ }
+ .BarGraphItem--bun .BarGraphBar {
+ background-color: #f9f1e1;
+ box-shadow: inset 1px 1px 3px #ccc6bb;
+ background-image: url(data:image/svg+xml;base64,PHN2ZyBpZD0iQnVuIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA4MCA3MCI+PHRpdGxlPkJ1biBMb2dvPC90aXRsZT48cGF0aCBpZD0iU2hhZG93IiBkPSJNNzEuMDksMjAuNzRjLS4xNi0uMTctLjMzLS4zNC0uNS0uNXMtLjMzLS4zNC0uNS0uNS0uMzMtLjM0LS41LS41LS4zMy0uMzQtLjUtLjUtLjMzLS4zNC0uNS0uNS0uMzMtLjM0LS41LS41LS4zMy0uMzQtLjUtLjVBMjYuNDYsMjYuNDYsMCwwLDEsNzUuNSwzNS43YzAsMTYuNTctMTYuODIsMzAuMDUtMzcuNSwzMC4wNS0xMS41OCwwLTIxLjk0LTQuMjMtMjguODMtMTAuODZsLjUuNS41LjUuNS41LjUuNS41LjUuNS41LjUuNUMxOS41NSw2NS4zLDMwLjE0LDY5Ljc1LDQyLDY5Ljc1YzIwLjY4LDAsMzcuNS0xMy40OCwzNy41LTMwQzc5LjUsMzIuNjksNzYuNDYsMjYsNzEuMDksMjAuNzRaIi8+PGcgaWQ9IkJvZHkiPjxwYXRoIGlkPSJCYWNrZ3JvdW5kIiBkPSJNNzMsMzUuN2MwLDE1LjIxLTE1LjY3LDI3LjU0LTM1LDI3LjU0UzMsNTAuOTEsMywzNS43QzMsMjYuMjcsOSwxNy45NCwxOC4yMiwxM1MzMy4xOCwzLDM4LDNzOC45NCw0LjEzLDE5Ljc4LDEwQzY3LDE3Ljk0LDczLDI2LjI3LDczLDM1LjdaIiBzdHlsZT0iZmlsbDojZmJmMGRmIi8+PHBhdGggaWQ9IkJvdHRvbV9TaGFkb3ciIGRhdGEtbmFtZT0iQm90dG9tIFNoYWRvdyIgZD0iTTczLDM1LjdhMjEuNjcsMjEuNjcsMCwwLDAtLjgtNS43OGMtMi43MywzMy4zLTQzLjM1LDM0LjktNTkuMzIsMjQuOTRBNDAsNDAsMCwwLDAsMzgsNjMuMjRDNTcuMyw2My4yNCw3Myw1MC44OSw3MywzNS43WiIgc3R5bGU9ImZpbGw6I2Y2ZGVjZSIvPjxwYXRoIGlkPSJMaWdodF9TaGluZSIgZGF0YS1uYW1lPSJMaWdodCBTaGluZSIgZD0iTTI0LjUzLDExLjE3QzI5LDguNDksMzQuOTQsMy40Niw0MC43OCwzLjQ1QTkuMjksOS4yOSwwLDAsMCwzOCwzYy0yLjQyLDAtNSwxLjI1LTguMjUsMy4xMy0xLjEzLjY2LTIuMywxLjM5LTMuNTQsMi4xNS0yLjMzLDEuNDQtNSwzLjA3LTgsNC43QzguNjksMTguMTMsMywyNi42MiwzLDM1LjdjMCwuNCwwLC44LDAsMS4xOUM5LjA2LDE1LjQ4LDIwLjA3LDEzLjg1LDI0LjUzLDExLjE3WiIgc3R5bGU9ImZpbGw6I2ZmZmVmYyIvPjxwYXRoIGlkPSJUb3AiIGQ9Ik0zNS4xMiw1LjUzQTE2LjQxLDE2LjQxLDAsMCwxLDI5LjQ5LDE4Yy0uMjguMjUtLjA2LjczLjMuNTksMy4zNy0xLjMxLDcuOTItNS4yMyw2LTEzLjE0QzM1LjcxLDUsMzUuMTIsNS4xMiwzNS4xMiw1LjUzWm0yLjI3LDBBMTYuMjQsMTYuMjQsMCwwLDEsMzksMTljLS4xMi4zNS4zMS42NS41NS4zNkM0MS43NCwxNi41Niw0My42NSwxMSwzNy45Myw1LDM3LjY0LDQuNzQsMzcuMTksNS4xNCwzNy4zOSw1LjQ5Wm0yLjc2LS4xN0ExNi40MiwxNi40MiwwLDAsMSw0NywxNy4xMmEuMzMuMzMsMCwwLDAsLjY1LjExYy45Mi0zLjQ5LjQtOS40NC03LjE3LTEyLjUzQzQwLjA4LDQuNTQsMzkuODIsNS4wOCw0MC4xNSw1LjMyWk0yMS42OSwxNS43NmExNi45NCwxNi45NCwwLDAsMCwxMC40Ny05Yy4xOC0uMzYuNzUtLjIyLjY2LjE4LTEuNzMsOC03LjUyLDkuNjctMTEuMTIsOS40NUMyMS4zMiwxNi40LDIxLjMzLDE1Ljg3LDIxLjY5LDE1Ljc2WiIgc3R5bGU9ImZpbGw6I2NjYmVhNztmaWxsLXJ1bGU6ZXZlbm9kZCIvPjxwYXRoIGlkPSJPdXRsaW5lIiBkPSJNMzgsNjUuNzVDMTcuMzIsNjUuNzUuNSw1Mi4yNy41LDM1LjdjMC0xMCw2LjE4LTE5LjMzLDE2LjUzLTI0LjkyLDMtMS42LDUuNTctMy4yMSw3Ljg2LTQuNjIsMS4yNi0uNzgsMi40NS0xLjUxLDMuNi0yLjE5QzMyLDEuODksMzUsLjUsMzgsLjVzNS42MiwxLjIsOC45LDMuMTRjMSwuNTcsMiwxLjE5LDMuMDcsMS44NywyLjQ5LDEuNTQsNS4zLDMuMjgsOSw1LjI3QzY5LjMyLDE2LjM3LDc1LjUsMjUuNjksNzUuNSwzNS43LDc1LjUsNTIuMjcsNTguNjgsNjUuNzUsMzgsNjUuNzVaTTM4LDNjLTIuNDIsMC01LDEuMjUtOC4yNSwzLjEzLTEuMTMuNjYtMi4zLDEuMzktMy41NCwyLjE1LTIuMzMsMS40NC01LDMuMDctOCw0LjdDOC42OSwxOC4xMywzLDI2LjYyLDMsMzUuNywzLDUwLjg5LDE4LjcsNjMuMjUsMzgsNjMuMjVTNzMsNTAuODksNzMsMzUuN0M3MywyNi42Miw2Ny4zMSwxOC4xMyw1Ny43OCwxMyw1NCwxMSw1MS4wNSw5LjEyLDQ4LjY2LDcuNjRjLTEuMDktLjY3LTIuMDktMS4yOS0zLTEuODRDNDIuNjMsNCw0MC40MiwzLDM4LDNaIi8+PC9nPjxnIGlkPSJNb3V0aCI+PGcgaWQ9IkJhY2tncm91bmQtMiIgZGF0YS1uYW1lPSJCYWNrZ3JvdW5kIj48cGF0aCBkPSJNNDUuMDUsNDNhOC45Myw4LjkzLDAsMCwxLTIuOTIsNC43MSw2LjgxLDYuODEsMCwwLDEtNCwxLjg4QTYuODQsNi44NCwwLDAsMSwzNCw0Ny43MSw4LjkzLDguOTMsMCwwLDEsMzEuMTIsNDNhLjcyLjcyLDAsMCwxLC44LS44MUg0NC4yNkEuNzIuNzIsMCwwLDEsNDUuMDUsNDNaIiBzdHlsZT0iZmlsbDojYjcxNDIyIi8+PC9nPjxnIGlkPSJUb25ndWUiPjxwYXRoIGlkPSJCYWNrZ3JvdW5kLTMiIGRhdGEtbmFtZT0iQmFja2dyb3VuZCIgZD0iTTM0LDQ3Ljc5YTYuOTEsNi45MSwwLDAsMCw0LjEyLDEuOSw2LjkxLDYuOTEsMCwwLDAsNC4xMS0xLjksMTAuNjMsMTAuNjMsMCwwLDAsMS0xLjA3LDYuODMsNi44MywwLDAsMC00LjktMi4zMSw2LjE1LDYuMTUsMCwwLDAtNSwyLjc4QzMzLjU2LDQ3LjQsMzMuNzYsNDcuNiwzNCw0Ny43OVoiIHN0eWxlPSJmaWxsOiNmZjYxNjQiLz48cGF0aCBpZD0iT3V0bGluZS0yIiBkYXRhLW5hbWU9Ik91dGxpbmUiIGQ9Ik0zNC4xNiw0N2E1LjM2LDUuMzYsMCwwLDEsNC4xOS0yLjA4LDYsNiwwLDAsMSw0LDEuNjljLjIzLS4yNS40NS0uNTEuNjYtLjc3YTcsNywwLDAsMC00LjcxLTEuOTMsNi4zNiw2LjM2LDAsMCwwLTQuODksMi4zNkE5LjUzLDkuNTMsMCwwLDAsMzQuMTYsNDdaIi8+PC9nPjxwYXRoIGlkPSJPdXRsaW5lLTMiIGRhdGEtbmFtZT0iT3V0bGluZSIgZD0iTTM4LjA5LDUwLjE5YTcuNDIsNy40MiwwLDAsMS00LjQ1LTIsOS41Miw5LjUyLDAsMCwxLTMuMTEtNS4wNSwxLjIsMS4yLDAsMCwxLC4yNi0xLDEuNDEsMS40MSwwLDAsMSwxLjEzLS41MUg0NC4yNmExLjQ0LDEuNDQsMCwwLDEsMS4xMy41MSwxLjE5LDEuMTksMCwwLDEsLjI1LDFoMGE5LjUyLDkuNTIsMCwwLDEtMy4xMSw1LjA1QTcuNDIsNy40MiwwLDAsMSwzOC4wOSw1MC4xOVptLTYuMTctNy40Yy0uMTYsMC0uMi4wNy0uMjEuMDlhOC4yOSw4LjI5LDAsMCwwLDIuNzMsNC4zN0E2LjIzLDYuMjMsMCwwLDAsMzguMDksNDlhNi4yOCw2LjI4LDAsMCwwLDMuNjUtMS43Myw4LjMsOC4zLDAsMCwwLDIuNzItNC4zNy4yMS4yMSwwLDAsMC0uMi0uMDlaIi8+PC9nPjxnIGlkPSJGYWNlIj48ZWxsaXBzZSBpZD0iUmlnaHRfQmx1c2giIGRhdGEtbmFtZT0iUmlnaHQgQmx1c2giIGN4PSI1My4yMiIgY3k9IjQwLjE4IiByeD0iNS44NSIgcnk9IjMuNDQiIHN0eWxlPSJmaWxsOiNmZWJiZDAiLz48ZWxsaXBzZSBpZD0iTGVmdF9CbHVjaCIgZGF0YS1uYW1lPSJMZWZ0IEJsdWNoIiBjeD0iMjIuOTUiIGN5PSI0MC4xOCIgcng9IjUuODUiIHJ5PSIzLjQ0IiBzdHlsZT0iZmlsbDojZmViYmQwIi8+PHBhdGggaWQ9IkV5ZXMiIGQ9Ik0yNS43LDM4LjhhNS41MSw1LjUxLDAsMSwwLTUuNS01LjUxQTUuNTEsNS41MSwwLDAsMCwyNS43LDM4LjhabTI0Ljc3LDBBNS41MSw1LjUxLDAsMSwwLDQ1LDMzLjI5LDUuNSw1LjUsMCwwLDAsNTAuNDcsMzguOFoiIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZCIvPjxwYXRoIGlkPSJJcmlzIiBkPSJNMjQsMzMuNjRhMi4wNywyLjA3LDAsMSwwLTIuMDYtMi4wN0EyLjA3LDIuMDcsMCwwLDAsMjQsMzMuNjRabTI0Ljc3LDBhMi4wNywyLjA3LDAsMSwwLTIuMDYtMi4wN0EyLjA3LDIuMDcsMCwwLDAsNDguNzUsMzMuNjRaIiBzdHlsZT0iZmlsbDojZmZmO2ZpbGwtcnVsZTpldmVub2RkIi8+PC9nPjwvc3ZnPg==);
+ background-repeat: no-repeat;
+ background-size: 56px 48.8px;
+ background-position: 6px 20%;
+ }
+ .BarGraph--vertical .BarGraphItem--bun {
+ border-top-right-radius: 12px;
+ border-bottom-right-radius: 12px;
+ }
+ .BarGraph--horizontal .BarGraphItem--bun {
+ border-top-left-radius: 12px;
+ border-top-right-radius: 12px;
+ }
+ .BarGraph--vertical .BarGraphBar {
+ height: var(--primary);
+ width: var(--opposite);
+ transform: scaleX(var(--level));
+ transform-origin: bottom left;
+ max-height: 40px;
+ margin-top: 1rem;
+ margin-bottom: 1rem;
+ }
+ .BarGraph--vertical .BarGraphList,
+ .BarGraph--vertical .BarGraphKey--vertical {
+ grid-template-columns: 1fr;
+ grid-template-rows: repeat(var(--count), 1fr);
+ }
+ .BarGraph--vertical .BarGraphList {
+ direction: rtl;
+ }
+ .BarGraphKeyItem-label {
+ color: #fff;
+ }
+ .BarGraphKeyItem-value {
+ color: #7a7a7a;
+ margin-top: 0.5rem;
+ }
+ .BarGraphKeyItem-viewSource {
+ margin-top: 0.5rem;
+ color: #7a7a7a;
+ text-transform: lowercase;
+ font-weight: thin;
+ font-size: 0.8rem;
+ }
+ .BarGraphKeyItem:hover {
+ text-decoration: none;
+ }
+ .BarGraphKeyItem:hover .BarGraphKeyItem-viewSource {
+ color: var(--orange-light);
+ }
+ .DemphasizedLabel {
+ text-transform: uppercase;
+ white-space: nowrap;
+ }
+ #frameworks {
+ display: flex;
+ }
+ .FrameworksGroup {
+ display: grid;
+ grid-template-rows: auto 40px;
+ gap: 0.5rem;
+ }
+ .DemphasizedLabel {
+ color: #7a7a7a;
+ font-weight: 300;
+ }
+ .FrameworksList {
+ display: grid;
+ grid-template-columns: repeat(2, 40px);
+ gap: 3.5rem;
+ align-items: center;
+ }
+ #cards {
+ display: grid;
+ }
+ #explain ul {
+ font-size: 1.2rem;
+ }
+ #explain li {
+ margin-bottom: 1rem;
+ line-height: var(--line-height);
+ }
+ .Tag {
+ --background: rgba(31, 31, 132, 0.15);
+ background-color: var(--background);
+ border-radius: 8px;
+ padding: 3px 8px;
+ color: #000;
+ text-decoration: none !important;
+ display: inline-block;
+ font-family: var(--monospace-font) !important;
+ }
+ .mono {
+ font-family: var(--monospace-font);
+ }
+ .Tag--Command {
+ --background: #111;
+ font-weight: medium;
+ color: #a3ff85;
+ }
+ .Tag--Command:before {
+ content: "\276f"/ "";
+ color: #ffffff59;
+ margin-top: auto;
+ margin-bottom: auto;
+ margin-right: 1ch;
+ margin-left: 0.5ch;
+ display: inline-block;
+ transform: translateY(-1px);
+ }
+ .Tag--WebAPI {
+ --background: #29b6f6;
+ box-shadow: inset -1px -1px 3px #e7bb49;
+ }
+ .Tag--NodeJS {
+ --background: rgb(130, 172, 108);
+ }
+ .Tag--TypeScript {
+ --background: rgb(69, 119, 192);
+ color: #fff;
+ }
+ .Tag--React {
+ color: #82d8f7;
+ --background: #333;
+ }
+ .Tag--React:before {
+ color: #82d8f780;
+ content: "<" / "";
+ }
+ .Tag--React:after {
+ color: #82d8f780;
+ content: ">" / "";
+ }
+ .Tag--Bun {
+ --background: #e600e5;
+ color: #fff;
+ }
+ .mono {
+ font-family: var(--monospace-font);
+ border-radius: 6px;
+ color: #006713;
+ }
+ @media (min-width: 931px) {
+ .InstallBox--mobile {
+ display: none;
+ }
+ }
+ #explain {
+ max-width: 650px;
+ margin: 0 auto;
+ }
+ @media (max-width: 930px) {
+ header {
+ padding: 24px 16px;
+ }
+ .InstallBox--desktop {
+ display: none;
+ }
+ #logo {
+ width: 48px;
+ }
+ :root {
+ --max-width: 100%;
+ --horizontal-padding: 16px;
+ --vertical-padding: 2rem;
+ --line-height: 1.6;
+ }
+ main {
+ grid-template-columns: auto;
+ grid-template-rows: auto auto auto;
+ }
+ #explain li {
+ line-height: var(--line-height);
+ margin-bottom: 1.5rem;
+ }
+ ul {
+ padding: 0;
+ list-style: none;
+ }
+ .Tabs {
+ margin-left: 0;
+ }
+ .Graphs,
+ .BarGraph,
+ .BarGraphList {
+ max-width: auto;
+ }
+ .BarGraph {
+ padding: 24px 0;
+ }
+ #pitch-content {
+ max-width: auto;
+ }
+ #pitch main {
+ gap: 1rem;
+ }
+ .InstallBox {
+ margin-top: 0;
+ }
+ .tagline {
+ font-size: 32pt;
+ }
+ #logo-text,
+ #HeaderInstallButton {
+ display: none;
+ }
+ }
+ .InstallBox--mobile {
+ border-radius: 0;
+ }
+ @media (max-width: 599px) {
+ .InstallBox-copy {
+ display: none;
+ }
+ .InstallBox-code-box {
+ font-size: 0.8rem;
+ }
+ }
+ @media (max-width: 360px) {
+ .tagline {
+ font-size: 22pt;
+ }
+ }
+ #explain p {
+ line-height: var(--line-height);
+ font-size: 1.2rem;
+ }
+ #explain p a {
+ text-decoration: underline;
+ }
+ .Zig {
+ transform: translateY(15%);
+ }
+ .CodeBlock .shiki {
+ padding: 1rem;
+ border-radius: 8px;
+ font-family: var(--monospace-font);
+ font-size: 1rem;
+ }
+ .Identifier {
+ font-family: var(--monospace-font);
+ font-size: 1rem;
+ color: #50fa7b !important;
+ background-color: #282a36;
+ padding: 0.25rem;
+ border-radius: 8px;
+ text-decoration: none !important;
+ }
+ .PerformanceClaim {
+ text-decoration: dashed underline 2px #000 !important;
+ text-decoration-skip-ink: auto !important;
+ }
+ .BarGraph--react,
+ .BarGraph--websocket,
+ .BarGraph--sqlite {
+ display: none;
+ }
+ .Graphs--active-react .BarGraph--react,
+ .Graphs--active-websocket .BarGraph--websocket,
+ .Graphs--active-sqlite .BarGraph--sqlite {
+ display: block;
+ }
+ @media (min-width: 930px) {
+ .Graphs {
+ margin-left: auto;
+ }
+ .BarGraph-subheading,
+ .BarGraph-heading {
+ text-align: center;
+ }
+ .BarGraph-heading {
+ margin-bottom: 0.25rem;
+ }
+ .BarGraphKeyItem-label {
+ width: 130px;
+ }
+ }
+ @media (max-width: 929px) {
+ .InstallBox-code-box {
+ width: fit-content;
+ }
+ .CodeBlock .shiki {
+ padding: 24px 16px;
+ margin: calc(-1 * var(--horizontal-padding));
+ width: calc(100vw - var(--horizontal-padding) - var(--horizontal-padding) -2px);
+ white-space: pre-wrap;
+ box-sizing: border-box;
+ border-radius: 0;
+ font-size: 0.8rem;
+ }
+ .logo-link {
+ gap: 0;
+ }
+ header {
+ grid-template-columns: min-content auto;
+ gap: 2rem;
+ }
+ .tagline,
+ .subtitle,
+ .BarGraph-heading,
+ .BarGraph-subheading {
+ padding: 0 var(--horizontal-padding);
+ }
+ main {
+ padding-left: 0;
+ padding-right: 0;
+ text-align: left;
+ }
+ .InstallBox {
+ padding: 24px 16px;
+ margin-bottom: -32px;
+ }
+ .tagline {
+ font-size: 30pt;
+ }
+ .Tag--Command {
+ display: block;
+ width: fit-content;
+ margin-bottom: 1rem;
+ }
+ .Tabs {
+ margin: 0;
+ gap: 0rem;
+ width: 100%;
+ border-top: 1px solid rgba(200, 200, 200, 0.1);
+ }
+ .Tab {
+ width: 100%;
+ border-bottom-color: #333;
+ }
+ #pitch-content {
+ max-width: 100%;
+ }
+ .Graphs--active-react .Tab[data-tab="react"],
+ .Graphs--active-sqlite .Tab[data-tab="sqlite"],
+ .Graphs--active-websocket .Tab[data-tab="websocket"] {
+ background-color: #6464641a;
+ }
+ }
+ #explain p > code {
+ white-space: pre;
+ padding: 1px 2px;
+ }
+ .Group {
+ display: block;
+ }
+ .Tag--Command {
+ display: block;
+ width: fit-content;
+ margin-bottom: 0.5rem;
+ padding: 8px 12px;
+ }
+ .Label-replace {
+ font-weight: 500;
+ }
+ .Label-text {
+ margin-top: 0.5rem;
+ margin-bottom: 1rem;
+ }
+ #batteries {
+ padding-left: 0;
+ }
+ .Group {
+ margin-bottom: 2rem;
+ }
+ .Group strong {
+ display: block;
+ }
+ .Built {
+ text-align: center;
+ margin-top: 4rem;
+ margin-bottom: 2rem;
+ color: #333;
+ }
+ img {
+ object-fit: contain;
+ }
+ .visually-hidden {
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ height: 1px;
+ overflow: hidden;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="header-wrap">
+ <header>
+ <a href="/" id="logo-link" aria-label="home"
+ ><img
+ height="61px"
+ src="data:image/svg+xml;base64, PHN2ZyBpZD0iQnVuIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA4MCA3MCI+PHRpdGxlPkJ1biBMb2dvPC90aXRsZT48cGF0aCBpZD0iU2hhZG93IiBkPSJNNzEuMDksMjAuNzRjLS4xNi0uMTctLjMzLS4zNC0uNS0uNXMtLjMzLS4zNC0uNS0uNS0uMzMtLjM0LS41LS41LS4zMy0uMzQtLjUtLjUtLjMzLS4zNC0uNS0uNS0uMzMtLjM0LS41LS41LS4zMy0uMzQtLjUtLjVBMjYuNDYsMjYuNDYsMCwwLDEsNzUuNSwzNS43YzAsMTYuNTctMTYuODIsMzAuMDUtMzcuNSwzMC4wNS0xMS41OCwwLTIxLjk0LTQuMjMtMjguODMtMTAuODZsLjUuNS41LjUuNS41LjUuNS41LjUuNS41LjUuNUMxOS41NSw2NS4zLDMwLjE0LDY5Ljc1LDQyLDY5Ljc1YzIwLjY4LDAsMzcuNS0xMy40OCwzNy41LTMwQzc5LjUsMzIuNjksNzYuNDYsMjYsNzEuMDksMjAuNzRaIi8+PGcgaWQ9IkJvZHkiPjxwYXRoIGlkPSJCYWNrZ3JvdW5kIiBkPSJNNzMsMzUuN2MwLDE1LjIxLTE1LjY3LDI3LjU0LTM1LDI3LjU0UzMsNTAuOTEsMywzNS43QzMsMjYuMjcsOSwxNy45NCwxOC4yMiwxM1MzMy4xOCwzLDM4LDNzOC45NCw0LjEzLDE5Ljc4LDEwQzY3LDE3Ljk0LDczLDI2LjI3LDczLDM1LjdaIiBzdHlsZT0iZmlsbDojZmJmMGRmIi8+PHBhdGggaWQ9IkJvdHRvbV9TaGFkb3ciIGRhdGEtbmFtZT0iQm90dG9tIFNoYWRvdyIgZD0iTTczLDM1LjdhMjEuNjcsMjEuNjcsMCwwLDAtLjgtNS43OGMtMi43MywzMy4zLTQzLjM1LDM0LjktNTkuMzIsMjQuOTRBNDAsNDAsMCwwLDAsMzgsNjMuMjRDNTcuMyw2My4yNCw3Myw1MC44OSw3MywzNS43WiIgc3R5bGU9ImZpbGw6I2Y2ZGVjZSIvPjxwYXRoIGlkPSJMaWdodF9TaGluZSIgZGF0YS1uYW1lPSJMaWdodCBTaGluZSIgZD0iTTI0LjUzLDExLjE3QzI5LDguNDksMzQuOTQsMy40Niw0MC43OCwzLjQ1QTkuMjksOS4yOSwwLDAsMCwzOCwzYy0yLjQyLDAtNSwxLjI1LTguMjUsMy4xMy0xLjEzLjY2LTIuMywxLjM5LTMuNTQsMi4xNS0yLjMzLDEuNDQtNSwzLjA3LTgsNC43QzguNjksMTguMTMsMywyNi42MiwzLDM1LjdjMCwuNCwwLC44LDAsMS4xOUM5LjA2LDE1LjQ4LDIwLjA3LDEzLjg1LDI0LjUzLDExLjE3WiIgc3R5bGU9ImZpbGw6I2ZmZmVmYyIvPjxwYXRoIGlkPSJUb3AiIGQ9Ik0zNS4xMiw1LjUzQTE2LjQxLDE2LjQxLDAsMCwxLDI5LjQ5LDE4Yy0uMjguMjUtLjA2LjczLjMuNTksMy4zNy0xLjMxLDcuOTItNS4yMyw2LTEzLjE0QzM1LjcxLDUsMzUuMTIsNS4xMiwzNS4xMiw1LjUzWm0yLjI3LDBBMTYuMjQsMTYuMjQsMCwwLDEsMzksMTljLS4xMi4zNS4zMS42NS41NS4zNkM0MS43NCwxNi41Niw0My42NSwxMSwzNy45Myw1LDM3LjY0LDQuNzQsMzcuMTksNS4xNCwzNy4zOSw1LjQ5Wm0yLjc2LS4xN0ExNi40MiwxNi40MiwwLDAsMSw0NywxNy4xMmEuMzMuMzMsMCwwLDAsLjY1LjExYy45Mi0zLjQ5LjQtOS40NC03LjE3LTEyLjUzQzQwLjA4LDQuNTQsMzkuODIsNS4wOCw0MC4xNSw1LjMyWk0yMS42OSwxNS43NmExNi45NCwxNi45NCwwLDAsMCwxMC40Ny05Yy4xOC0uMzYuNzUtLjIyLjY2LjE4LTEuNzMsOC03LjUyLDkuNjctMTEuMTIsOS40NUMyMS4zMiwxNi40LDIxLjMzLDE1Ljg3LDIxLjY5LDE1Ljc2WiIgc3R5bGU9ImZpbGw6I2NjYmVhNztmaWxsLXJ1bGU6ZXZlbm9kZCIvPjxwYXRoIGlkPSJPdXRsaW5lIiBkPSJNMzgsNjUuNzVDMTcuMzIsNjUuNzUuNSw1Mi4yNy41LDM1LjdjMC0xMCw2LjE4LTE5LjMzLDE2LjUzLTI0LjkyLDMtMS42LDUuNTctMy4yMSw3Ljg2LTQuNjIsMS4yNi0uNzgsMi40NS0xLjUxLDMuNi0yLjE5QzMyLDEuODksMzUsLjUsMzgsLjVzNS42MiwxLjIsOC45LDMuMTRjMSwuNTcsMiwxLjE5LDMuMDcsMS44NywyLjQ5LDEuNTQsNS4zLDMuMjgsOSw1LjI3QzY5LjMyLDE2LjM3LDc1LjUsMjUuNjksNzUuNSwzNS43LDc1LjUsNTIuMjcsNTguNjgsNjUuNzUsMzgsNjUuNzVaTTM4LDNjLTIuNDIsMC01LDEuMjUtOC4yNSwzLjEzLTEuMTMuNjYtMi4zLDEuMzktMy41NCwyLjE1LTIuMzMsMS40NC01LDMuMDctOCw0LjdDOC42OSwxOC4xMywzLDI2LjYyLDMsMzUuNywzLDUwLjg5LDE4LjcsNjMuMjUsMzgsNjMuMjVTNzMsNTAuODksNzMsMzUuN0M3MywyNi42Miw2Ny4zMSwxOC4xMyw1Ny43OCwxMyw1NCwxMSw1MS4wNSw5LjEyLDQ4LjY2LDcuNjRjLTEuMDktLjY3LTIuMDktMS4yOS0zLTEuODRDNDIuNjMsNCw0MC40MiwzLDM4LDNaIi8+PC9nPjxnIGlkPSJNb3V0aCI+PGcgaWQ9IkJhY2tncm91bmQtMiIgZGF0YS1uYW1lPSJCYWNrZ3JvdW5kIj48cGF0aCBkPSJNNDUuMDUsNDNhOC45Myw4LjkzLDAsMCwxLTIuOTIsNC43MSw2LjgxLDYuODEsMCwwLDEtNCwxLjg4QTYuODQsNi44NCwwLDAsMSwzNCw0Ny43MSw4LjkzLDguOTMsMCwwLDEsMzEuMTIsNDNhLjcyLjcyLDAsMCwxLC44LS44MUg0NC4yNkEuNzIuNzIsMCwwLDEsNDUuMDUsNDNaIiBzdHlsZT0iZmlsbDojYjcxNDIyIi8+PC9nPjxnIGlkPSJUb25ndWUiPjxwYXRoIGlkPSJCYWNrZ3JvdW5kLTMiIGRhdGEtbmFtZT0iQmFja2dyb3VuZCIgZD0iTTM0LDQ3Ljc5YTYuOTEsNi45MSwwLDAsMCw0LjEyLDEuOSw2LjkxLDYuOTEsMCwwLDAsNC4xMS0xLjksMTAuNjMsMTAuNjMsMCwwLDAsMS0xLjA3LDYuODMsNi44MywwLDAsMC00LjktMi4zMSw2LjE1LDYuMTUsMCwwLDAtNSwyLjc4QzMzLjU2LDQ3LjQsMzMuNzYsNDcuNiwzNCw0Ny43OVoiIHN0eWxlPSJmaWxsOiNmZjYxNjQiLz48cGF0aCBpZD0iT3V0bGluZS0yIiBkYXRhLW5hbWU9Ik91dGxpbmUiIGQ9Ik0zNC4xNiw0N2E1LjM2LDUuMzYsMCwwLDEsNC4xOS0yLjA4LDYsNiwwLDAsMSw0LDEuNjljLjIzLS4yNS40NS0uNTEuNjYtLjc3YTcsNywwLDAsMC00LjcxLTEuOTMsNi4zNiw2LjM2LDAsMCwwLTQuODksMi4zNkE5LjUzLDkuNTMsMCwwLDAsMzQuMTYsNDdaIi8+PC9nPjxwYXRoIGlkPSJPdXRsaW5lLTMiIGRhdGEtbmFtZT0iT3V0bGluZSIgZD0iTTM4LjA5LDUwLjE5YTcuNDIsNy40MiwwLDAsMS00LjQ1LTIsOS41Miw5LjUyLDAsMCwxLTMuMTEtNS4wNSwxLjIsMS4yLDAsMCwxLC4yNi0xLDEuNDEsMS40MSwwLDAsMSwxLjEzLS41MUg0NC4yNmExLjQ0LDEuNDQsMCwwLDEsMS4xMy41MSwxLjE5LDEuMTksMCwwLDEsLjI1LDFoMGE5LjUyLDkuNTIsMCwwLDEtMy4xMSw1LjA1QTcuNDIsNy40MiwwLDAsMSwzOC4wOSw1MC4xOVptLTYuMTctNy40Yy0uMTYsMC0uMi4wNy0uMjEuMDlhOC4yOSw4LjI5LDAsMCwwLDIuNzMsNC4zN0E2LjIzLDYuMjMsMCwwLDAsMzguMDksNDlhNi4yOCw2LjI4LDAsMCwwLDMuNjUtMS43Myw4LjMsOC4zLDAsMCwwLDIuNzItNC4zNy4yMS4yMSwwLDAsMC0uMi0uMDlaIi8+PC9nPjxnIGlkPSJGYWNlIj48ZWxsaXBzZSBpZD0iUmlnaHRfQmx1c2giIGRhdGEtbmFtZT0iUmlnaHQgQmx1c2giIGN4PSI1My4yMiIgY3k9IjQwLjE4IiByeD0iNS44NSIgcnk9IjMuNDQiIHN0eWxlPSJmaWxsOiNmZWJiZDAiLz48ZWxsaXBzZSBpZD0iTGVmdF9CbHVjaCIgZGF0YS1uYW1lPSJMZWZ0IEJsdWNoIiBjeD0iMjIuOTUiIGN5PSI0MC4xOCIgcng9IjUuODUiIHJ5PSIzLjQ0IiBzdHlsZT0iZmlsbDojZmViYmQwIi8+PHBhdGggaWQ9IkV5ZXMiIGQ9Ik0yNS43LDM4LjhhNS41MSw1LjUxLDAsMSwwLTUuNS01LjUxQTUuNTEsNS41MSwwLDAsMCwyNS43LDM4LjhabTI0Ljc3LDBBNS41MSw1LjUxLDAsMSwwLDQ1LDMzLjI5LDUuNSw1LjUsMCwwLDAsNTAuNDcsMzguOFoiIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZCIvPjxwYXRoIGlkPSJJcmlzIiBkPSJNMjQsMzMuNjRhMi4wNywyLjA3LDAsMSwwLTIuMDYtMi4wN0EyLjA3LDIuMDcsMCwwLDAsMjQsMzMuNjRabTI0Ljc3LDBhMi4wNywyLjA3LDAsMSwwLTIuMDYtMi4wN0EyLjA3LDIuMDcsMCwwLDAsNDguNzUsMzMuNjRaIiBzdHlsZT0iZmlsbDojZmZmO2ZpbGwtcnVsZTpldmVub2RkIi8+PC9nPjwvc3ZnPg=="
+ alt="Bun logo"
+ id="logo" /><img
+ alt="Bun"
+ id="logo-text"
+ height="31.65px"
+ src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAALkAAAA9CAYAAADxjMiSAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAi1SURBVHgB7Z3vddpIEMBnBPhdPgUqiLgUYFJBSAXxVRC7gjgVxK4gdgVxKohdQUgFRxo4SAXgT753Ejs3s5JsQiQQsCsJaX/v+R+SkZBmZ2fnnxAKgGYTf/W1AKCL/JW2fxtgGv86x15/Dg7HHmDeHVlQRSC7IYBP/AWxkKKHL/R2osfX9N/RPqZOcsrvN0XEqVL0g/8e80AYuwHgyMMvQi6CHETCOfBADQi9F7zDgCLB7UL1GCOp7wTebafXH4HDkcKjkAfzyTf+MYBqCvNGRNvzt1FLwSVr+Ck4HDHLQk5QExDhxgm7I8GDGkIEpwuEb//NJqfgaDxayONFZa2QhS9r9M/B/eQjOBqNFvJ/D9QOzwXBRXg/+QyOxlJLc2UVMV9Yo38CRyNphJBrCM6D2WQIjsbRhgYhNjr/6IOjckhUnGM0Q/61yzEaH7zW88dtin6SRL93DAJqF6LWcOyNgCZA8GZT4Ii9MhIv0DGD5IJzRDcJiKWmI9ByKgLifOXGTHd1Z2aciy/bMCWqnHEeU4kjbPrcImgLPpaKPqO/Es1ORaLQoBb3CrypF0Wm53kDc+LwCD14j2xObhkhH7EJ+qXDP/Nc18YJOd+Um/Zz/yxr+wPf6DbCBEwek29Gu9t/AzsQzKd8X2gIBmBhOmv3+jdg+VibjqOPJV4vNiFhX6cHwkVbwfU67d4cmzyGtdIJOFJByNbY26EGWVtEiQSzf8bi9QITXj1+H46J/J2WBJhg3SaPwu04SqZNHlVzmdKSTMMkG1G+FNCJh/jaZHJXCl25IC4aao8FeH7a62KeSJCOAH0wiMhLyIIeziYyg9yubrcl5HM+8jW/+c2WwqRPUCKVHsJHW8Ie1DkuUAFaCMdprwdAVwjogx26hPCVZefVUa8/Xt5gx1xhAefFx8Wu2pJP8ibgBSL/OgYLYEOFXBX0uUU5rUbRRXHxeugdWAZlplgxXSprkz/jARIS/AUyKxiG1rxnq8YDYNPgNjlzBivvJTMzFEM3jFzFT8eWb5Zt4J0RQeeV+hcwTOfJzfYbTdXypvEit6dGtHjBMjZcDvwdgndlBGZxFUWF8ORhYRPiPRTN0sxReSFXa7TuLrBdaMXOd/xK4mF5iOzjARTPozavvJCbNh9aii7BYZ3Ew8JrnNLiEghKH7vyQm60IJqjnU32jydh+iKQ+yaalE0V6x6VzHNATx+78glasb/cCE6LZ/OwJmK4M+WninRloFVakwf3P40FhIjgg4tyNg+Wn0ElNXmcnfYRSJ2DCUhdHvVeXoGjbMSrNeWop/ZuEZD17hAs5MNKCblEqli434WSnUaGPjwLeKf38iLv7rLQrU3bgoqAUWrsZVoKbjibnPAOn2z50WUBbEfI2S8aziepCw7KcAlK/rJ05wJzEjYXE4U1+M02/xTnUjsMoDufRWm3o6x94oSqWxb2K7LgT5fBY0uTdyl7GvIh/WSMIV4UWWQ6G7w8RMBbBG/y3gMW9vNgPj02lTu/TK3yyWValMofKYpwAl4uinZo7kR2vF+1EnJF6q5tOEJaPnS4bfu2JLLZcQSGqZcmR+9TiDAJ5pOZ9HbUiUE2/L8F0rSEMST6AYaxZJPjLRHdZWzM7EvOYdguR6lew/65DvL+Em0bLlijBPeTm87zvgsEHQBKEujALHaEnEfj0YZC1nVI9K0FdGEiyZ4ij80Fe3tO8yyE4vI8R0mgBXOzkuaK5JEf9f48ZeE0pn1F2BcpVSMp+7k03BIJmyLkCVJCx2aPsaKJJUF3hREN4gAWnmg0HK8ru73sUiwbmoRctVGpVF7I48prsyYEwXnB2twHR2kchAsRLdjJIaQnfz2zE0Tqxu3eHCVwEEJuZbqPXJXpmyyYLEmViqN4Ki/ksQa0YVoMszZY8bCg935bE0kS/qvaSeGQOABNTmZyytPeOcOdSATGo24Q9QP5mnfnuAlr7v0d2Xjxt0r6hkWLF9F1KQVbFf3DYD5Z25xSP0tVnooRlY45r4wBdMTT9PSMHuxdMFumJov7bNtiEOfX3PKM8R2ern0XEd6GYka5kKtRrIT15Rk9HEYXIR2Rgi/bPB0gskPpFAqo8s4K8bcARiFY54SF2i1GC8Ba+VucMyLCeioCw5pL+7vlyQTSxnl53zgxS7T/EHQCVyG1OZkmiQxIHqRTt+irB0XWeGo/MYfpYVWGqYT1L5L6vnYHgjsoo71ZDF8iyeTsA+IxGGJVuSzzByugAmavUmjckyYSWuCtTReguFd6WbQIPpDhlIZ11Lk/ZNLVtlGZd5jjgUpxZXlZ12Ucn58tL0+j0EK+aJiQs5Y8y7UjwTWUAMXH7dSulK8cmmeukMpdYMsLFjEXClcAnbhdtZgQ6AR9b5ol5EQ/tmo0xEJGBgs3ch1zpSkpGax5bGpBSGOEXDRiG3AIW3LU61/ZqCDPYrUpKbtTp2CITZHtus4ajRByWWiyHf5qVw9CSHRWiACkmFLKLT73Rgv5sxo34mF/+LU8DXkfF5lcH2XpIV0JMhAzTCkn5HtSW02uNa900+q9NJLFKBVKocwGNjQ6292taBD9hkkPCzV0EVtHIdeNPll79ztrGk3ugmh0eb6oyeJqPdPwWiFrpinSw1LXheljWJ+1VL+lc0doiFEo+aDKteI+iNdpj502SWzanT7MJhf79IZJ2hnzTDPauLM8QMDDt/zT5z/8LXJq5lHpII6BFj85yjtdexi+fh7iW+JjQHSMrVJ9o8HIC2WUZp80hR2Q9IIFD3zA1jFFLfJ82CHlGKNMUsmVym5WpBvhs6BLp34PlC8H3fIC20Zu4Jht5btO9HjzUrSQfl68Vg5quOYaRc3n+YIrxW5MA+cbVxnpm588Rr2zpIlNNTyV3Pcgo+uZCFF8zLnt65/k4IcZ8rfUA/O3c9kp3U8XM0TtmX3UX6rLN1iyCLsy+pJtYIjlUSlCIsK9Tfquo9lYz2ld1TiYc+pJRqZrwezYl/8BpjOlthQ26tQAAAAASUVORK5CYII="
+ /></a>
+ <nav class="Navigation">
+ <ul>
+ <li>
+ <a class="NavText" href="https://github.com/oven-sh/bun#Reference">Docs</a>
+ </li>
+ <li>
+ <a class="NavText" href="https://bun.sh/discord">Discord</a>
+ </li>
+ <li>
+ <a class="NavText" href="https://github.com/oven-sh/bun">GitHub</a>
+ </li>
+ </ul>
+ </nav>
+ </header>
+ </div>
+ <div id="pitch">
+ <main>
+ <div id="pitch-content">
+ <h1 class="tagline">Bun is a fast all-in-one JavaScript runtime</h1>
+ <p class="subtitle">
+ Bundle, transpile, install and run JavaScript &amp; TypeScript projects — all in Bun. Bun is a new
+ JavaScript runtime with a native bundler, transpiler, task runner and npm client built-in.
+ </p>
+ <div class="InstallBox InstallBox--desktop">
+ <div class="InstallBox-label">
+ <div class="InstallBox-label-heading">
+ Install Bun CLI
+ <!-- -->0.2.1<!-- -->
+ (beta)
+ </div>
+ <div class="InstallBox-label-subtitle">
+ macOS x64 &amp; Silicon, Linux x64, Windows Subsystem for Linux
+ </div>
+ </div>
+ <div class="InstallBox-code-box">
+ <div class="InstallBox-curl">curl https://bun.sh/install | bash</div>
+ <button class="InstallBox-copy" aria-label="Copy installation script">copy</button>
+ </div>
+ <a class="InstallBox-view-source-link" target="_blank" href="https://bun.sh/install">Show script source</a>
+ </div>
+ </div>
+ <div class="Graphs Graphs--active-react">
+ <div class="Tabs" role="tablist">
+ <button
+ data-tab="react"
+ id="tab-react"
+ aria-controls="react-tab-content"
+ class="Tab"
+ role="tab"
+ aria-selected="true"
+ tabindex="0"
+ >
+ Bun.serve</button
+ ><button
+ data-tab="websocket"
+ id="tab-websocket"
+ aria-controls="websocket-tab-content"
+ class="Tab"
+ role="tab"
+ tabindex="-1"
+ >
+ WebSocket</button
+ ><button
+ data-tab="sqlite"
+ id="tab-sqlite"
+ aria-controls="sqlite-tab-content"
+ class="Tab"
+ role="tab"
+ tabindex="-1"
+ >
+ bun:sqlite
+ </button>
+ </div>
+ <div id="active-tab" class="ActiveTab">
+ <div
+ role="tabpanel"
+ tabindex="0"
+ id="react-tab-content"
+ aria-labelledby="tab-react"
+ class="BarGraph BarGraph--react BarGraph--horizontal BarGraph--dark"
+ >
+ <h2 class="BarGraph-heading">Server-side rendering React</h2>
+ <p class="BarGraph-subheading">HTTP requests per second (Linux x64)</p>
+ <ul style="--count: 3" class="BarGraphList">
+ <li class="BarGraphItem BarGraphItem--bun" style="--amount: 69845; --max: 87306.25">
+ <div class="visually-hidden">bun: 69,845 requests per second</div>
+ <div style="--amount: 69845; --max: 87306.25" class="BarGraphBar" aria-hidden="true">
+ <div style="--amount: 69845; --max: 87306.25" class="BarGraphBar-label">69,845</div>
+ </div>
+ </li>
+ <li class="BarGraphItem BarGraphItem--node" style="--amount: 16288; --max: 87306.25">
+ <div class="visually-hidden">node: 16,288 requests per second</div>
+ <div style="--amount: 16288; --max: 87306.25" class="BarGraphBar" aria-hidden="true">
+ <div style="--amount: 16288; --max: 87306.25" class="BarGraphBar-label">16,288</div>
+ </div>
+ </li>
+ <li class="BarGraphItem BarGraphItem--deno" style="--amount: 12926; --max: 87306.25">
+ <div class="visually-hidden">deno: 12,926 requests per second</div>
+ <div style="--amount: 12926; --max: 87306.25" class="BarGraphBar" aria-hidden="true">
+ <div style="--amount: 12926; --max: 87306.25" class="BarGraphBar-label">12,926</div>
+ </div>
+ </li>
+ </ul>
+ <div style="--count: 3" class="BarGraphKey">
+ <a
+ href="https://github.com/oven-sh/bun/blob/b0a7f8df926e91d3b2f0b3b8833ddaf55073f30e/bench/react-hello-world/react-hello-world.jsx#L27"
+ target="_blank"
+ class="BarGraphKeyItem"
+ aria-label="bun benchmark source"
+ ><div class="BarGraphKeyItem-label">bun</div>
+ <div class="BarGraphKeyItem-value">v0.2.0</div>
+ <div class="BarGraphKeyItem-viewSource">View source</div></a
+ ><a
+ href="https://github.com/oven-sh/bun/blob/e55d6eed2bf9a5db30250fdd8b9be063dc949054/bench/react-hello-world/react-hello-world.node.jsx"
+ target="_blank"
+ class="BarGraphKeyItem"
+ aria-label="node benchmark source"
+ ><div class="BarGraphKeyItem-label">node</div>
+ <div class="BarGraphKeyItem-value">v18.1.0</div>
+ <div class="BarGraphKeyItem-viewSource">View source</div></a
+ ><a
+ href="https://github.com/oven-sh/bun/blob/af033c02c5fbaade201abfe332f376879d9e6885/bench/react-hello-world/react-hello-world.deno.jsx"
+ target="_blank"
+ class="BarGraphKeyItem"
+ aria-label="Deno.serve() benchmark source"
+ ><div class="BarGraphKeyItem-label">Deno.serve()</div>
+ <div class="BarGraphKeyItem-value">v1.26.0</div>
+ <div class="BarGraphKeyItem-viewSource">View source</div></a
+ >
+ </div>
+ </div>
+ <div
+ role="tabpanel"
+ tabindex="-1"
+ id="websocket-tab-content"
+ aria-labelledby="tab-websocket"
+ class="BarGraph BarGraph--websocket BarGraph--horizontal BarGraph--dark"
+ >
+ <h2 class="BarGraph-heading">WebSocket server chat</h2>
+ <p class="BarGraph-subheading">Messages sent per second (Linux x64, 16 clients)</p>
+ <ul style="--count: 3" class="BarGraphList">
+ <li class="BarGraphItem BarGraphItem--bun" style="--amount: 737280; --max: 921600">
+ <div class="visually-hidden">bun: 737,280 messages sent per second</div>
+ <div style="--amount: 737280; --max: 921600" class="BarGraphBar" aria-hidden="true">
+ <div style="--amount: 737280; --max: 921600" class="BarGraphBar-label">737,280</div>
+ </div>
+ </li>
+ <li class="BarGraphItem BarGraphItem--node" style="--amount: 107457; --max: 921600">
+ <div class="visually-hidden">node: 107,457 messages sent per second</div>
+ <div style="--amount: 107457; --max: 921600" class="BarGraphBar" aria-hidden="true">
+ <div style="--amount: 107457; --max: 921600" class="BarGraphBar-label">107,457</div>
+ </div>
+ </li>
+ <li class="BarGraphItem BarGraphItem--deno" style="--amount: 82097; --max: 921600">
+ <div class="visually-hidden">deno: 82,097 messages sent per second</div>
+ <div style="--amount: 82097; --max: 921600" class="BarGraphBar" aria-hidden="true">
+ <div style="--amount: 82097; --max: 921600" class="BarGraphBar-label">82,097</div>
+ </div>
+ </li>
+ </ul>
+ <div style="--count: 3" class="BarGraphKey">
+ <a
+ href="https://github.com/oven-sh/bun/blob/9c7eb75a9ac845d92bfdfd6cc574dc8f39bde293/bench/websocket-server/chat-server.bun.js#L1"
+ target="_blank"
+ class="BarGraphKeyItem"
+ aria-label="Bun.serve() benchmark source"
+ ><div class="BarGraphKeyItem-label">Bun.serve()</div>
+ <div class="BarGraphKeyItem-value">v0.2.1</div>
+ <div class="BarGraphKeyItem-viewSource">View source</div></a
+ ><a
+ href="https://github.com/oven-sh/bun/blob/9c7eb75a9ac845d92bfdfd6cc574dc8f39bde293/bench/websocket-server/chat-server.node.mjs#L1"
+ target="_blank"
+ class="BarGraphKeyItem"
+ aria-label="ws (Node.js) benchmark source"
+ ><div class="BarGraphKeyItem-label">ws (Node.js)</div>
+ <div class="BarGraphKeyItem-value">node v18.10.0</div>
+ <div class="BarGraphKeyItem-viewSource">View source</div></a
+ ><a
+ href="https://github.com/oven-sh/bun/blob/9c7eb75a9ac845d92bfdfd6cc574dc8f39bde293/bench/websocket-server/chat-server.deno.mjs#L1"
+ target="_blank"
+ class="BarGraphKeyItem"
+ aria-label="Deno.serve() benchmark source"
+ ><div class="BarGraphKeyItem-label">Deno.serve()</div>
+ <div class="BarGraphKeyItem-value">v1.26.2</div>
+ <div class="BarGraphKeyItem-viewSource">View source</div></a
+ >
+ </div>
+ </div>
+ <div
+ role="tabpanel"
+ tabindex="-1"
+ id="sqlite-tab-content"
+ aria-labelledby="tab-sqlite"
+ class="BarGraph--sqlite BarGraph BarGraph--horizontal BarGraph--dark"
+ >
+ <h2 class="BarGraph-heading">Load a huge table</h2>
+ <p class="BarGraph-subheading">Average queries per second</p>
+ <ul style="--count: 3" class="BarGraphList">
+ <li class="BarGraphItem BarGraphItem--bun" style="--amount: 70.32; --max: 88">
+ <div class="visually-hidden">bun: 70.32 queries per second</div>
+ <div style="--amount: 70.32; --max: 88" class="BarGraphBar" aria-hidden="true">
+ <div style="--amount: 70.32; --max: 88" class="BarGraphBar-label">70.32</div>
+ </div>
+ </li>
+ <li class="BarGraphItem BarGraphItem--deno" style="--amount: 36.54; --max: 88">
+ <div class="visually-hidden">deno: 36.54 queries per second</div>
+ <div style="--amount: 36.54; --max: 88" class="BarGraphBar" aria-hidden="true">
+ <div style="--amount: 36.54; --max: 88" class="BarGraphBar-label">36.54</div>
+ </div>
+ </li>
+ <li class="BarGraphItem BarGraphItem--better-sqlite3" style="--amount: 23.28; --max: 88">
+ <div class="visually-hidden">better-sqlite3: 23.28 queries per second</div>
+ <div style="--amount: 23.28; --max: 88" class="BarGraphBar" aria-hidden="true">
+ <div style="--amount: 23.28; --max: 88" class="BarGraphBar-label">23.28</div>
+ </div>
+ </li>
+ </ul>
+ <div style="--count: 3" class="BarGraphKey">
+ <a
+ href="https://github.com/oven-sh/bun/blob/b0a7f8df926e91d3b2f0b3b8833ddaf55073f30e/bench/sqlite/bun.js#L9"
+ target="_blank"
+ class="BarGraphKeyItem"
+ aria-label="bun:sqlite benchmark source"
+ ><div class="BarGraphKeyItem-label">bun:sqlite</div>
+ <div class="BarGraphKeyItem-value">v0.2.0</div>
+ <div class="BarGraphKeyItem-viewSource">View source</div></a
+ ><a
+ href="https://github.com/oven-sh/bun/blob/6223030360c121e272aad98c7d1c14a009c5fc1c/bench/sqlite/deno.js#L9"
+ target="_blank"
+ class="BarGraphKeyItem"
+ aria-label="deno (x/sqlite3) benchmark source"
+ ><div class="BarGraphKeyItem-label">deno (x/sqlite3)</div>
+ <div class="BarGraphKeyItem-value">v1.26.1</div>
+ <div class="BarGraphKeyItem-viewSource">View source</div></a
+ ><a
+ href="https://github.com/oven-sh/bun/blob/e55d6eed2bf9a5db30250fdd8b9be063dc949054/bench/sqlite/node.mjs"
+ target="_blank"
+ class="BarGraphKeyItem"
+ aria-label="better-sqlite3 benchmark source"
+ ><div class="BarGraphKeyItem-label">better-sqlite3</div>
+ <div class="BarGraphKeyItem-value">node v18.2.0</div>
+ <div class="BarGraphKeyItem-viewSource">View source</div></a
+ >
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="InstallBox InstallBox--mobile">
+ <div class="InstallBox-label">
+ <div class="InstallBox-label-heading">
+ Install Bun CLI
+ <!-- -->0.2.1<!-- -->
+ (beta)
+ </div>
+ <div class="InstallBox-label-subtitle">macOS x64 &amp; Silicon, Linux x64, Windows Subsystem for Linux</div>
+ </div>
+ <div class="InstallBox-code-box">
+ <div class="InstallBox-curl">curl https://bun.sh/install | bash</div>
+ <button class="InstallBox-copy" aria-label="Copy installation script">copy</button>
+ </div>
+ <a class="InstallBox-view-source-link" target="_blank" href="https://bun.sh/install">Show script source</a>
+ </div>
+ </main>
+ </div>
+ <section id="explain-section">
+ <div id="explain">
+ <h2>Tell me more about Bun</h2>
+ <p>
+ Bun is a modern JavaScript runtime like Node or Deno. It was built from scratch to focus on three main things:
+ </p>
+ <ul>
+ <li>Start fast (it has the edge in mind).</li>
+ <li>New levels of performance (extending JavaScriptCore, the engine).</li>
+ <li>Being a great and complete tool (bundler, transpiler, package manager).</li>
+ </ul>
+ <p>
+ Bun is designed as a drop-in replacement for your current JavaScript &amp; TypeScript apps or scripts — on
+ your local computer, server or on the edge. Bun natively implements hundreds of Node.js and Web APIs,
+ including ~90% of<!-- -->
+ <a href="https://nodejs.org/api/n-api.html" target="_blank">Node-API</a>
+ <!-- -->functions (native modules), fs, path, Buffer and more.
+ </p>
+ <p>
+ The goal of Bun is to run most of the world&#x27;s JavaScript outside of browsers, bringing performance and
+ complexity enhancements to your future infrastructure, as well as developer productivity through better,
+ simpler tooling.
+ </p>
+ <h2>Batteries included</h2>
+ <ul id="batteries">
+ <li>
+ Web APIs like<!-- -->
+ <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/fetch" class="Tag Tag--WebAPI"
+ >fetch</a
+ >,<!-- -->
+ <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket" class="Tag Tag--WebAPI"
+ >WebSocket</a
+ >, and<!-- -->
+ <a
+ target="_blank"
+ href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream"
+ class="Tag Tag--WebAPI"
+ >ReadableStream</a
+ >
+ <!-- -->are built-in
+ </li>
+ <li>
+ <span target="_blank" class="Tag Tag--NodeJS">node_modules</span>
+ bun implements Node.js&#x27; module resolution algorithm, so you can use npm packages in Bun. ESM and
+ CommonJS are supported, but Bun internally uses ESM
+ </li>
+ <li>
+ In Bun, every file is transpiled.<!-- -->
+ <span target="_blank" class="Tag Tag--TypeScript">TypeScript</span>
+ &amp; <span target="_blank" class="Tag Tag--React">JSX</span> just work
+ </li>
+ <li>
+ Bun supports <code>&quot;paths&quot;</code>, <code>&quot;jsxImportSource&quot;</code>and more from
+ <span target="_blank" class="Tag Tag--TypeScript">tsconfig.json</span>
+ files
+ </li>
+ <li>
+ <span target="_blank" class="Tag Tag--Bun">Bun.Transpiler</span>
+ Bun&#x27;s JSX &amp; TypeScript transpiler is available as an API in Bun
+ </li>
+ <li>
+ use the fastest system calls available with
+ <span target="_blank" class="Tag Tag--Bun">Bun.write</span>
+ <!-- -->to write, copy, pipe, send and clone files
+ </li>
+ <li>
+ Bun automatically loads environment variables from
+ <span target="_blank" class="Tag Tag--Bun">.env</span>
+ <!-- -->files. No more<!-- -->
+ <code class="mono">require(&quot;dotenv&quot;).config()</code>
+ </li>
+ <li>
+ Bun ships with a fast SQLite3 client built-in<!-- -->
+ <span target="_blank" class="Tag Tag--Bun">bun:sqlite</span>
+ </li>
+ <li>
+ <a target="_blank" href="https://github.com/oven-sh/bun/issues/158" class="Tag Tag--NodeJS">Node-API</a>
+ <!-- -->Bun implements most of<!-- -->
+ <a href="https://nodejs.org/api/n-api.html#node-api" target="_blank">Node-API (N-API)</a>. Many Node.js
+ native modules just work
+ </li>
+ <li>
+ <span target="_blank" class="Tag Tag--Bun">bun:ffi</span> call native code from JavaScript with Bun&#x27;s
+ low-overhead foreign function interface
+ </li>
+ <li>
+ <span target="_blank" class="Tag Tag--NodeJS">node:fs</span>
+ <span target="_blank" class="Tag Tag--NodeJS">node:path</span> Bun natively supports a growing list of
+ Node.js core modules along with globals like Buffer and process
+ </li>
+ </ul>
+ <h2>How does Bun work?</h2>
+ <p>
+ Bun uses the<!-- -->
+ <a href="https://github.com/WebKit/WebKit/tree/main/Source/JavaScriptCore">JavaScriptCore</a>
+ <!-- -->engine, which tends<!-- -->
+ <a target="blank" href="https://twitter.com/jarredsumner/status/1499225725492076544">to start</a>
+ <!-- -->and perform a little faster than more traditional choices like V8. Bun is written in<!-- -->
+ <a href="https://ziglang.org/"
+ ><svg xmlns="http://www.w3.org/2000/svg" height="1.2rem" class="Zig" viewBox="0 0 400 140">
+ <title>Zig</title>
+ <g fill="#F7A41D">
+ <g>
+ <polygon points="46,22 28,44 19,30"></polygon>
+ <polygon
+ points="46,22 33,33 28,44 22,44 22,95 31,95 20,100 12,117 0,117 0,22"
+ shape-rendering="crispEdges"
+ ></polygon>
+ <polygon points="31,95 12,117 4,106"></polygon>
+ </g>
+ <g>
+ <polygon points="56,22 62,36 37,44"></polygon>
+ <polygon points="56,22 111,22 111,44 37,44 56,32" shape-rendering="crispEdges"></polygon>
+ <polygon points="116,95 97,117 90,104"></polygon>
+ <polygon points="116,95 100,104 97,117 42,117 42,95" shape-rendering="crispEdges"></polygon>
+ <polygon points="150,0 52,117 3,140 101,22"></polygon>
+ </g>
+ <g>
+ <polygon points="141,22 140,40 122,45"></polygon>
+ <polygon
+ points="153,22 153,117 106,117 120,105 125,95 131,95 131,45 122,45 132,36 141,22"
+ shape-rendering="crispEdges"
+ ></polygon>
+ <polygon points="125,95 130,110 106,117"></polygon>
+ </g>
+ </g>
+ <g fill="#121212">
+ <g>
+ <polygon points="260,22 260,37 229,40 177,40 177,22" shape-rendering="crispEdges"></polygon>
+ <polygon points="260,37 207,99 207,103 176,103 229,40 229,37"></polygon>
+ <polygon points="261,99 261,117 176,117 176,103 206,99" shape-rendering="crispEdges"></polygon>
+ </g>
+ <rect x="272" y="22" shape-rendering="crispEdges" width="22" height="95"></rect>
+ <g>
+ <polygon points="394,67 394,106 376,106 376,81 360,70 346,67" shape-rendering="crispEdges"></polygon>
+ <polygon points="360,68 376,81 346,67"></polygon>
+ <path
+ d="M394,106c-10.2,7.3-24,12-37.7,12c-29,0-51.1-20.8-51.1-48.3c0-27.3,22.5-48.1,52-48.1
+ c14.3,0,29.2,5.5,38.9,14l-13,15c-7.1-6.3-16.8-10-25.9-10c-17,0-30.2,12.9-30.2,29.5c0,16.8,13.3,29.6,30.3,29.6
+ c5.7,0,12.8-2.3,19-5.5L394,106z"
+ ></path>
+ </g>
+ </g></svg></a
+ >, a low-level programming language with manual memory management.<br /><br />Most of Bun is written from
+ scratch including the JSX/TypeScript transpiler, npm client, bundler, SQLite client, HTTP client, WebSocket
+ client and more.
+ </p>
+ <h2>Why is Bun fast?</h2>
+ <p>
+ An enormous amount of time spent profiling, benchmarking and optimizing things. The answer is different for
+ every part of Bun, but one general theme:<!-- -->
+ <a href="https://ziglang.org/"
+ ><svg xmlns="http://www.w3.org/2000/svg" height="1.2rem" class="Zig" viewBox="0 0 400 140">
+ <title>Zig</title>
+ <g fill="#F7A41D">
+ <g>
+ <polygon points="46,22 28,44 19,30"></polygon>
+ <polygon
+ points="46,22 33,33 28,44 22,44 22,95 31,95 20,100 12,117 0,117 0,22"
+ shape-rendering="crispEdges"
+ ></polygon>
+ <polygon points="31,95 12,117 4,106"></polygon>
+ </g>
+ <g>
+ <polygon points="56,22 62,36 37,44"></polygon>
+ <polygon points="56,22 111,22 111,44 37,44 56,32" shape-rendering="crispEdges"></polygon>
+ <polygon points="116,95 97,117 90,104"></polygon>
+ <polygon points="116,95 100,104 97,117 42,117 42,95" shape-rendering="crispEdges"></polygon>
+ <polygon points="150,0 52,117 3,140 101,22"></polygon>
+ </g>
+ <g>
+ <polygon points="141,22 140,40 122,45"></polygon>
+ <polygon
+ points="153,22 153,117 106,117 120,105 125,95 131,95 131,45 122,45 132,36 141,22"
+ shape-rendering="crispEdges"
+ ></polygon>
+ <polygon points="125,95 130,110 106,117"></polygon>
+ </g>
+ </g>
+ <g fill="#121212">
+ <g>
+ <polygon points="260,22 260,37 229,40 177,40 177,22" shape-rendering="crispEdges"></polygon>
+ <polygon points="260,37 207,99 207,103 176,103 229,40 229,37"></polygon>
+ <polygon points="261,99 261,117 176,117 176,103 206,99" shape-rendering="crispEdges"></polygon>
+ </g>
+ <rect x="272" y="22" shape-rendering="crispEdges" width="22" height="95"></rect>
+ <g>
+ <polygon points="394,67 394,106 376,106 376,81 360,70 346,67" shape-rendering="crispEdges"></polygon>
+ <polygon points="360,68 376,81 346,67"></polygon>
+ <path
+ d="M394,106c-10.2,7.3-24,12-37.7,12c-29,0-51.1-20.8-51.1-48.3c0-27.3,22.5-48.1,52-48.1
+ c14.3,0,29.2,5.5,38.9,14l-13,15c-7.1-6.3-16.8-10-25.9-10c-17,0-30.2,12.9-30.2,29.5c0,16.8,13.3,29.6,30.3,29.6
+ c5.7,0,12.8-2.3,19-5.5L394,106z"
+ ></path>
+ </g>
+ </g></svg></a
+ >&#x27;s low-level control over memory and lack of hidden control flow makes it much simpler to write fast
+ software.<!-- -->
+ <a href="https://github.com/sponsors/ziglang">Sponsor the Zig Software Foundation</a>.
+ </p>
+ <h2>Getting started</h2>
+ <p>
+ To install Bun, run this<!-- -->
+ <a target="_blank" href="https://bun.sh/install">install script</a>
+ <!-- -->in your terminal. It downloads Bun from GitHub.
+ </p>
+ <div class="CodeBlock">
+ <pre
+ class="shiki"
+ style="background-color: #282a36"
+ ><code><span class="line"><span style="color: #F8F8F2">curl https://bun.sh/install </span><span style="color: #FF79C6">|</span><span style="color: #F8F8F2"> bash</span></span></code></pre>
+ </div>
+ <p>
+ <!-- -->Bun&#x27;s HTTP server is built on web standards like<!-- -->
+ <a class="Identifier" href="https://developer.mozilla.org/en-US/docs/Web/API/Request">Request</a>
+ <!-- -->and<!-- -->
+ <a class="Identifier" href="https://developer.mozilla.org/en-US/docs/Web/API/Response">Response</a>
+ </p>
+ <div class="CodeBlock">
+ <pre
+ class="shiki"
+ style="background-color: #282a36"
+ ><code><span class="line"><span style="color: #6272A4">// http.js</span></span>
+<span class="line"><span style="color: #FF79C6">export</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">default</span><span style="color: #F8F8F2"> {</span></span>
+<span class="line"><span style="color: #F8F8F2"> port</span><span style="color: #FF79C6">:</span><span style="color: #F8F8F2"> </span><span style="color: #BD93F9">3000</span><span style="color: #F8F8F2">,</span></span>
+<span class="line"><span style="color: #F8F8F2"> </span><span style="color: #50FA7B">fetch</span><span style="color: #F8F8F2">(</span><span style="color: #FFB86C; font-style: italic">request</span><span style="color: #F8F8F2">) {</span></span>
+<span class="line"><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">return</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6; font-weight: bold">new</span><span style="color: #F8F8F2"> </span><span style="color: #50FA7B">Response</span><span style="color: #F8F8F2">(</span><span style="color: #E9F284">&quot;</span><span style="color: #F1FA8C">Welcome to Bun!</span><span style="color: #E9F284">&quot;</span><span style="color: #F8F8F2">);</span></span>
+<span class="line"><span style="color: #F8F8F2"> },</span></span>
+<span class="line"><span style="color: #F8F8F2">};</span></span></code></pre>
+ </div>
+ <p>Run it with Bun:</p>
+ <div class="CodeBlock">
+ <pre
+ class="shiki"
+ style="background-color: #282a36"
+ ><code><span class="line"><span style="color: #F8F8F2">bun run http.js</span></span></code></pre>
+ </div>
+ <p>
+ Then open<!-- -->
+ <a target="_blank" href="http://localhost:3000">http://localhost:3000</a>
+ <!-- -->in your browser.<br /><br />See<!-- -->
+ <a href="https://github.com/oven-sh/bun/tree/main/examples">more examples</a>
+ <!-- -->and check out<!-- -->
+ <a href="https://github.com/oven-sh/bun#Reference">the docs</a>. If you have any questions or want help, join<!-- -->
+ <a href="https://bun.sh/discord">Bun&#x27;s Discord</a>.
+ </p>
+ <h2>Bun CLI</h2>
+ <div class="Group">
+ <span target="_blank" class="Tag Tag--Command">bun run</span>
+ <p>
+ The same command for running JavaScript &amp; TypeScript files with bun&#x27;s JavaScript runtime also runs
+ package.json<!-- -->
+ <code class="mono">&quot;scripts&quot;</code>.
+ </p>
+ <strong
+ >Replace <code class="mono">npm run</code> with<!-- -->
+ <code class="mono">bun run</code> and save 160ms on every run.</strong
+ ><br />
+ <div>
+ Bun runs package.json scripts<!-- -->
+ <a
+ href="https://twitter.com/jarredsumner/status/1454218996983623685"
+ target="_blank"
+ class="PerformanceClaim"
+ >30x faster than <code class="mono">npm run</code></a
+ >
+ </div>
+ </div>
+ <div class="Group">
+ <span target="_blank" class="Tag Tag--Command">bun install</span>
+ <p>
+ <code classsName="mono">bun install</code> is an npm-compatible package manager. You probably will be
+ surprised by how much faster copying files can get.
+ </p>
+ <strong
+ >Replace <code class="mono">yarn</code> with<!-- -->
+ <code class="mono">bun install</code> and get 20x faster package installs.</strong
+ ><br />
+ <div><code class="mono">bun install</code> uses the fastest system calls available to copy files.</div>
+ </div>
+ <div class="Group">
+ <span target="_blank" class="Tag Tag--Command">bun wiptest</span>
+ <p>A Jest-like test runner for JavaScript &amp; TypeScript projects built-in to Bun.</p>
+ <div class="Label">
+ <a
+ href="https://twitter.com/jarredsumner/status/1542824445810642946"
+ target="_blank"
+ class="PerformanceClaim"
+ >You&#x27;ve never seen a JavaScript test runner this fast</a
+ >
+ <!-- -->(or incomplete).
+ </div>
+ </div>
+ <h2>What is the license?</h2>
+ <p>MIT License, excluding dependencies which have various licenses.</p>
+ <h2>How do I see the source code?</h2>
+ <p>Bun is on <a href="https://github.com/oven-sh/bun">GitHub</a>.</p>
+ </div>
+ </section>
+ <section id="explain-section"><div id="explain"></div></section>
+ <script>
+ [...document.querySelectorAll(".Tab")].map(el => {
+ el.addEventListener("click", function (e) {
+ var tab = e.srcElement.getAttribute("data-tab");
+ [...document.querySelectorAll(".Tab")].map(el => {
+ var active = el.getAttribute("data-tab") === tab;
+ el.setAttribute("tabindex", active ? 0 : -1);
+ el.setAttribute("aria-selected", active);
+ });
+ [...document.querySelectorAll(".BarGraph")].map(el => {
+ var active = el.id === tab + "-tab-content";
+ el.setAttribute("tabindex", active ? 0 : -1);
+ });
+ document.querySelector(".Graphs").setAttribute("class", "Graphs Graphs--active-" + tab);
+ });
+
+ el.addEventListener("keydown", e => {
+ var tabs = [...document.querySelectorAll(".Tab")];
+ var activeTabEl = document.querySelector(".Tab[aria-selected='true']");
+ var activeTabIndex = tabs.indexOf(activeTabEl);
+ if (e.key === "ArrowRight" || e.key === "ArrowDown") {
+ e.preventDefault();
+ activeTabIndex = (activeTabIndex + 1) % tabs.length;
+ tabs[activeTabIndex].click();
+ tabs[activeTabIndex].focus();
+ }
+ if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
+ e.preventDefault();
+ activeTabIndex = (activeTabIndex + tabs.length - 1) % tabs.length;
+ tabs[activeTabIndex].click();
+ tabs[activeTabIndex].focus();
+ }
+ if (e.key === "Home") {
+ e.preventDefault();
+ tabs[0].click();
+ tabs[0].focus();
+ }
+ if (e.key === "End") {
+ e.preventDefault();
+ tabs[tabs.length - 1].click();
+ tabs[tabs.length - 1].focus();
+ }
+ });
+ });
+
+ for (const el of document.querySelectorAll(".InstallBox-copy")) {
+ el.addEventListener("click", async e => {
+ await navigator.clipboard.writeText("curl https://bun.sh/install | bash");
+ });
+ }
+ </script>
+ <div class="Built">
+ Built with Bun
+ <!-- -->0.2.1
+ </div>
+ </body>
+</html>
diff --git a/test/js/web/fetch/fixture.html.gz b/test/js/web/fetch/fixture.html.gz
new file mode 100644
index 000000000..0bb85d4cb
--- /dev/null
+++ b/test/js/web/fetch/fixture.html.gz
Binary files differ
diff --git a/test/js/web/html/FormData.test.ts b/test/js/web/html/FormData.test.ts
new file mode 100644
index 000000000..9d0db4361
--- /dev/null
+++ b/test/js/web/html/FormData.test.ts
@@ -0,0 +1,410 @@
+import { afterAll, beforeAll, describe, expect, it, test } from "bun:test";
+import fs, { chmodSync, unlinkSync } from "fs";
+import { mkfifo } from "mkfifo";
+import { gc, withoutAggressiveGC } from "../../gc";
+
+describe("FormData", () => {
+ it("should be able to append a string", () => {
+ const formData = new FormData();
+ formData.append("foo", "bar");
+ expect(formData.get("foo")).toBe("bar");
+ expect(formData.getAll("foo")[0]).toBe("bar");
+ });
+
+ it("should be able to append a Blob", async () => {
+ const formData = new FormData();
+ formData.append("foo", new Blob(["bar"]));
+ expect(await formData.get("foo").text()).toBe("bar");
+ expect(formData.getAll("foo")[0] instanceof Blob).toBe(true);
+ });
+
+ it("should be able to set a Blob", async () => {
+ const formData = new FormData();
+ formData.set("foo", new Blob(["bar"]));
+ expect(await formData.get("foo").text()).toBe("bar");
+ expect(formData.getAll("foo")[0] instanceof Blob).toBe(true);
+ });
+
+ it("should be able to set a string", async () => {
+ const formData = new FormData();
+ formData.set("foo", "bar");
+ expect(formData.get("foo")).toBe("bar");
+ expect(formData.getAll("foo")[0]).toBe("bar");
+ });
+
+ const multipartFormDataFixturesRawBody = [
+ {
+ name: "simple",
+ body: '--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n--foo--\r\n',
+ headers: {
+ "Content-Type": "multipart/form-data; boundary=foo",
+ },
+ expected: {
+ foo: "bar",
+ },
+ },
+ {
+ name: "simple with trailing CRLF",
+ body: '--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n--foo--\r\n\r\n',
+ headers: {
+ "Content-Type": "multipart/form-data; boundary=foo",
+ },
+ expected: {
+ foo: "bar",
+ },
+ },
+ {
+ name: "simple with trailing CRLF and extra CRLF",
+ body: '--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n--foo--\r\n\r\n\r\n',
+ headers: {
+ "Content-Type": "multipart/form-data; boundary=foo",
+ },
+ expected: {
+ foo: "bar",
+ },
+ },
+ {
+ name: "advanced",
+ body: '--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n--foo\r\nContent-Disposition: form-data; name="baz"\r\n\r\nqux\r\n--foo--\r\n',
+ headers: {
+ "Content-Type": "multipart/form-data; boundary=foo",
+ },
+ expected: {
+ foo: "bar",
+ baz: "qux",
+ },
+ },
+ {
+ name: "advanced with multiple values",
+ body: '--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbaz\r\n--foo--\r\n',
+ headers: {
+ "Content-Type": "multipart/form-data; boundary=foo",
+ },
+ expected: {
+ foo: ["bar", "baz"],
+ },
+ },
+ {
+ name: "advanced with multiple values and trailing CRLF",
+ body: '--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbaz\r\n--foo--\r\n\r\n',
+ headers: {
+ "Content-Type": "multipart/form-data; boundary=foo",
+ },
+ expected: {
+ foo: ["bar", "baz"],
+ },
+ },
+ {
+ name: "extremely advanced",
+ body: '--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n--foo\r\nContent-Disposition: form-data; name="baz"\r\n\r\nqux\r\n--foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbaz\r\n--foo--\r\n',
+ headers: {
+ "Content-Type": "multipart/form-data; boundary=foo",
+ },
+ expected: {
+ foo: ["bar", "baz"],
+ baz: "qux",
+ },
+ },
+ {
+ name: "with name and filename",
+ body: '--foo\r\nContent-Disposition: form-data; name="foo"; filename="bar"\r\n\r\nbaz\r\n--foo--\r\n',
+ headers: {
+ "Content-Type": "multipart/form-data; boundary=foo",
+ },
+ expected: {
+ foo: new Blob(["baz"]),
+ },
+ },
+ {
+ name: "with name and filename and trailing CRLF",
+ body: '--foo\r\nContent-Disposition: form-data; name="foo"; filename="bar"\r\n\r\nbaz\r\n--foo--\r\n\r\n',
+ headers: {
+ "Content-Type": "multipart/form-data; boundary=foo",
+ },
+ expected: {
+ foo: new Blob(["baz"]),
+ },
+ },
+ ];
+
+ for (const { name, body, headers, expected: expected_ } of multipartFormDataFixturesRawBody) {
+ const Class = [Response, Request] as const;
+ for (const C of Class) {
+ it(`should parse multipart/form-data (${name}) with ${C.name}`, async () => {
+ const response = C === Response ? new Response(body, { headers }) : new Request({ headers, body });
+ const formData = await response.formData();
+ expect(formData instanceof FormData).toBe(true);
+ const entry = {};
+ const expected = Object.assign({}, expected_);
+
+ for (const key of formData.keys()) {
+ const values = formData.getAll(key);
+ if (values.length > 1) {
+ entry[key] = values;
+ } else {
+ entry[key] = values[0];
+ if (entry[key] instanceof Blob) {
+ expect(expected[key] instanceof Blob).toBe(true);
+
+ entry[key] = await entry[key].text();
+ expected[key] = await expected[key].text();
+ } else {
+ expect(typeof entry[key]).toBe(typeof expected[key]);
+ expect(expected[key] instanceof Blob).toBe(false);
+ }
+ }
+ }
+
+ expect(entry).toEqual(expected);
+ });
+
+ it(`should roundtrip multipart/form-data (${name}) with ${C.name}`, async () => {
+ const response = C === Response ? new Response(body, { headers }) : new Request({ headers, body });
+ const formData = await response.formData();
+ expect(formData instanceof FormData).toBe(true);
+
+ const request = await new Response(formData).formData();
+ expect(request instanceof FormData).toBe(true);
+
+ const aKeys = Array.from(formData.keys());
+ const bKeys = Array.from(request.keys());
+ expect(aKeys).toEqual(bKeys);
+
+ for (const key of aKeys) {
+ const aValues = formData.getAll(key);
+ const bValues = request.getAll(key);
+ for (let i = 0; i < aValues.length; i++) {
+ const a = aValues[i];
+ const b = bValues[i];
+ if (a instanceof Blob) {
+ expect(b instanceof Blob).toBe(true);
+ expect(await a.text()).toBe(await b.text());
+ } else {
+ expect(a).toBe(b);
+ }
+ }
+ }
+
+ // Test that it also works with Blob.
+ const c = await new Blob([body], { type: headers["Content-Type"] }).formData();
+ expect(c instanceof FormData).toBe(true);
+ const cKeys = Array.from(c.keys());
+ expect(cKeys).toEqual(bKeys);
+ for (const key of cKeys) {
+ const cValues = c.getAll(key);
+ const bValues = request.getAll(key);
+ for (let i = 0; i < cValues.length; i++) {
+ const c = cValues[i];
+ const b = bValues[i];
+ if (c instanceof Blob) {
+ expect(b instanceof Blob).toBe(true);
+ expect(await c.text()).toBe(await b.text());
+ } else {
+ expect(c).toBe(b);
+ }
+ }
+ }
+ });
+ }
+ }
+
+ it("should throw on missing final boundary", async () => {
+ const response = new Response('-foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n', {
+ headers: {
+ "Content-Type": "multipart/form-data; boundary=foo",
+ },
+ });
+ try {
+ await response.formData();
+ throw "should have thrown";
+ } catch (e) {
+ expect(typeof e.message).toBe("string");
+ }
+ });
+
+ it("should throw on bad boundary", async () => {
+ const response = new Response('foo\r\nContent-Disposition: form-data; name="foo"\r\n\r\nbar\r\n', {
+ headers: {
+ "Content-Type": "multipart/form-data; boundary=foo",
+ },
+ });
+ try {
+ await response.formData();
+ throw "should have thrown";
+ } catch (e) {
+ expect(typeof e.message).toBe("string");
+ }
+ });
+
+ it("should throw on bad header", async () => {
+ const response = new Response('foo\r\nContent-Disposition: form-data; name"foo"\r\n\r\nbar\r\n', {
+ headers: {
+ "Content-Type": "multipart/form-data; boundary=foo",
+ },
+ });
+ try {
+ await response.formData();
+ throw "should have thrown";
+ } catch (e) {
+ expect(typeof e.message).toBe("string");
+ }
+ });
+
+ it("file upload on HTTP server (receive)", async () => {
+ const server = Bun.serve({
+ port: 0,
+ development: false,
+ async fetch(req) {
+ const formData = await req.formData();
+ return new Response(formData.get("foo"));
+ },
+ });
+
+ const reqBody = new Request(`http://${server.hostname}:${server.port}`, {
+ body: '--foo\r\nContent-Disposition: form-data; name="foo"; filename="bar"\r\n\r\nbaz\r\n--foo--\r\n\r\n',
+ headers: {
+ "Content-Type": "multipart/form-data; boundary=foo",
+ },
+ method: "POST",
+ });
+
+ const res = await fetch(reqBody);
+ const body = await res.text();
+ expect(body).toBe("baz");
+ server.stop(true);
+ });
+
+ it("file send on HTTP server (receive)", async () => {
+ const server = Bun.serve({
+ port: 0,
+ development: false,
+ async fetch(req) {
+ const formData = await req.formData();
+ return new Response(formData);
+ },
+ });
+
+ const reqBody = new Request(`http://${server.hostname}:${server.port}`, {
+ body: '--foo\r\nContent-Disposition: form-data; name="foo"; filename="bar"\r\n\r\nbaz\r\n--foo--\r\n\r\n',
+ headers: {
+ "Content-Type": "multipart/form-data; boundary=foo",
+ },
+ method: "POST",
+ });
+
+ const res = await fetch(reqBody);
+ const body = await res.formData();
+ expect(await (body.get("foo") as Blob).text()).toBe("baz");
+ server.stop(true);
+ });
+
+ describe("Bun.file support", () => {
+ describe("roundtrip", () => {
+ const path = import.meta.dir + "/form-data-fixture.txt";
+ for (const C of [Request, Response]) {
+ it(`with ${C.name}`, async () => {
+ await Bun.write(path, "foo!");
+ const formData = new FormData();
+ formData.append("foo", Bun.file(path));
+ const response = C === Response ? new Response(formData) : new Request({ body: formData });
+ expect(response.headers.get("content-type")?.startsWith("multipart/form-data;")).toBe(true);
+
+ const formData2 = await response.formData();
+ expect(formData2 instanceof FormData).toBe(true);
+ expect(formData2.get("foo") instanceof Blob).toBe(true);
+ expect(await (formData2.get("foo") as Blob).text()).toBe("foo!");
+ });
+ }
+ });
+
+ it("doesnt crash when file is missing", async () => {
+ const formData = new FormData();
+ formData.append("foo", Bun.file("missing"));
+ expect(() => new Response(formData)).toThrow();
+ });
+ });
+
+ it("Bun.inspect", () => {
+ const formData = new FormData();
+ formData.append("foo", "bar");
+ formData.append("foo", new Blob(["bar"]));
+ formData.append("bar", "baz");
+ formData.append("boop", Bun.file("missing"));
+ expect(Bun.inspect(formData).length > 0).toBe(true);
+ });
+
+ describe("URLEncoded", () => {
+ test("should parse URL encoded", async () => {
+ const response = new Response("foo=bar&baz=qux", {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ });
+ const formData = await response.formData();
+ expect(formData instanceof FormData).toBe(true);
+ expect(formData.get("foo")).toBe("bar");
+ expect(formData.get("baz")).toBe("qux");
+ });
+
+ test("should parse URLSearchParams", async () => {
+ const searchParams = new URLSearchParams("foo=bar&baz=qux");
+ const response = new Response(searchParams);
+ expect(response.headers.get("Content-Type")).toBe("application/x-www-form-urlencoded;charset=UTF-8");
+
+ expect(searchParams instanceof URLSearchParams).toBe(true);
+ expect(searchParams.get("foo")).toBe("bar");
+
+ const formData = await response.formData();
+ expect(formData instanceof FormData).toBe(true);
+ expect(formData.get("foo")).toBe("bar");
+ expect(formData.get("baz")).toBe("qux");
+ });
+
+ test("should parse URL encoded with charset", async () => {
+ const response = new Response("foo=bar&baz=qux", {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
+ },
+ });
+ const formData = await response.formData();
+ expect(formData instanceof FormData).toBe(true);
+ expect(formData.get("foo")).toBe("bar");
+ expect(formData.get("baz")).toBe("qux");
+ });
+
+ test("should parse URL encoded with charset and space", async () => {
+ const response = new Response("foo=bar&baz=qux+quux", {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
+ },
+ });
+ const formData = await response.formData();
+ expect(formData instanceof FormData).toBe(true);
+ expect(formData.get("foo")).toBe("bar");
+ expect(formData.get("baz")).toBe("qux quux");
+ });
+
+ test("should parse URL encoded with charset and plus", async () => {
+ const response = new Response("foo=bar&baz=qux+quux", {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
+ },
+ });
+ const formData = await response.formData();
+ expect(formData instanceof FormData).toBe(true);
+ expect(formData.get("foo")).toBe("bar");
+ expect(formData.get("baz")).toBe("qux quux");
+ });
+
+ it("should handle multiple values", async () => {
+ const response = new Response("foo=bar&foo=baz", {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ });
+ const formData = await response.formData();
+ expect(formData instanceof FormData).toBe(true);
+ expect(formData.getAll("foo")).toEqual(["bar", "baz"]);
+ });
+ });
+});
diff --git a/test/js/web/html/form-data-fixture.txt b/test/js/web/html/form-data-fixture.txt
new file mode 100644
index 000000000..a4d20dd78
--- /dev/null
+++ b/test/js/web/html/form-data-fixture.txt
@@ -0,0 +1 @@
+foo! \ No newline at end of file
diff --git a/test/js/web/streams/bun-streams-test-fifo.sh b/test/js/web/streams/bun-streams-test-fifo.sh
new file mode 100644
index 000000000..57650ba1d
--- /dev/null
+++ b/test/js/web/streams/bun-streams-test-fifo.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+echoerr() { echo "$@" 1>&2; }
+
+echoerr "bun-streams-test-fifo.sh: starting"
+echo -e "$FIFO_TEST" >>${@: -1}
+echoerr "bun-streams-test-fifo.sh: ending"
+exit 0
diff --git a/test/js/web/streams/fetch.js.txt b/test/js/web/streams/fetch.js.txt
new file mode 100644
index 000000000..5a9b52fcf
--- /dev/null
+++ b/test/js/web/streams/fetch.js.txt
@@ -0,0 +1,46 @@
+<!doctype html>
+<html>
+<head>
+ <title>Example Domain</title>
+
+ <meta charset="utf-8" />
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <style type="text/css">
+ body {
+ background-color: #f0f0f2;
+ margin: 0;
+ padding: 0;
+ font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+
+ }
+ div {
+ width: 600px;
+ margin: 5em auto;
+ padding: 2em;
+ background-color: #fdfdff;
+ border-radius: 0.5em;
+ box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
+ }
+ a:link, a:visited {
+ color: #38488f;
+ text-decoration: none;
+ }
+ @media (max-width: 700px) {
+ div {
+ margin: 0 auto;
+ width: auto;
+ }
+ }
+ </style>
+</head>
+
+<body>
+<div>
+ <h1>Example Domain</h1>
+ <p>This domain is for use in illustrative examples in documents. You may use this
+ domain in literature without prior coordination or asking for permission.</p>
+ <p><a href="https://www.iana.org/domains/example">More information...</a></p>
+</div>
+</body>
+</html>
diff --git a/test/js/web/streams/streams.test.js b/test/js/web/streams/streams.test.js
new file mode 100644
index 000000000..c4af85e4f
--- /dev/null
+++ b/test/js/web/streams/streams.test.js
@@ -0,0 +1,630 @@
+import { file, readableStreamToArrayBuffer, readableStreamToArray, readableStreamToText } from "bun";
+import { expect, it, beforeEach, afterEach, describe } from "bun:test";
+import { mkfifo } from "mkfifo";
+import { realpathSync, unlinkSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+import { tmpdir } from "os";
+import { gc } from "harness";
+
+beforeEach(() => gc());
+afterEach(() => gc());
+
+describe("WritableStream", () => {
+ it("works", async () => {
+ try {
+ var chunks = [];
+ var writable = new WritableStream({
+ write(chunk, controller) {
+ chunks.push(chunk);
+ },
+ close(er) {
+ console.log("closed");
+ console.log(er);
+ },
+ abort(reason) {
+ console.log("aborted!");
+ console.log(reason);
+ },
+ });
+
+ var writer = writable.getWriter();
+
+ writer.write(new Uint8Array([1, 2, 3]));
+
+ writer.write(new Uint8Array([4, 5, 6]));
+
+ await writer.close();
+
+ expect(JSON.stringify(Array.from(Buffer.concat(chunks)))).toBe(JSON.stringify([1, 2, 3, 4, 5, 6]));
+ } catch (e) {
+ console.log(e);
+ console.log(e.stack);
+ throw e;
+ }
+ });
+
+ it("pipeTo", async () => {
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue("hello world");
+ controller.close();
+ },
+ });
+
+ let received;
+ const ws = new WritableStream({
+ write(chunk, controller) {
+ received = chunk;
+ },
+ });
+ await rs.pipeTo(ws);
+ expect(received).toBe("hello world");
+ });
+});
+
+describe("ReadableStream.prototype.tee", () => {
+ it("class", () => {
+ const [a, b] = new ReadableStream().tee();
+ expect(a instanceof ReadableStream).toBe(true);
+ expect(b instanceof ReadableStream).toBe(true);
+ });
+
+ describe("default stream", () => {
+ it("works", async () => {
+ var [a, b] = new ReadableStream({
+ start(controller) {
+ controller.enqueue("a");
+ controller.enqueue("b");
+ controller.enqueue("c");
+ controller.close();
+ },
+ }).tee();
+
+ expect(await readableStreamToText(a)).toBe("abc");
+ expect(await readableStreamToText(b)).toBe("abc");
+ });
+ });
+
+ describe("direct stream", () => {
+ it("works", async () => {
+ try {
+ var [a, b] = new ReadableStream({
+ pull(controller) {
+ controller.write("a");
+ controller.write("b");
+ controller.write("c");
+ controller.close();
+ },
+ type: "direct",
+ }).tee();
+
+ expect(await readableStreamToText(a)).toBe("abc");
+ expect(await readableStreamToText(b)).toBe("abc");
+ } catch (e) {
+ console.log(e.message);
+ console.log(e.stack);
+ throw e;
+ }
+ });
+ });
+});
+
+it("ReadableStream.prototype[Symbol.asyncIterator]", async () => {
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue("hello");
+ controller.enqueue("world");
+ controller.close();
+ },
+ cancel(reason) {},
+ });
+
+ const chunks = [];
+ try {
+ for await (const chunk of stream) {
+ chunks.push(chunk);
+ }
+ } catch (e) {
+ console.log(e.message);
+ console.log(e.stack);
+ }
+
+ expect(chunks.join("")).toBe("helloworld");
+});
+
+it("ReadableStream.prototype[Symbol.asyncIterator] pull", async () => {
+ const stream = new ReadableStream({
+ pull(controller) {
+ controller.enqueue("hello");
+ controller.enqueue("world");
+ controller.close();
+ },
+ cancel(reason) {},
+ });
+
+ const chunks = [];
+ for await (const chunk of stream) {
+ chunks.push(chunk);
+ }
+ expect(chunks.join("")).toBe("helloworld");
+});
+
+it("ReadableStream.prototype[Symbol.asyncIterator] direct", async () => {
+ const stream = new ReadableStream({
+ pull(controller) {
+ controller.write("hello");
+ controller.write("world");
+ controller.close();
+ },
+ type: "direct",
+ cancel(reason) {},
+ });
+
+ const chunks = [];
+ try {
+ for await (const chunk of stream) {
+ chunks.push(chunk);
+ }
+ } catch (e) {
+ console.log(e.message);
+ console.log(e.stack);
+ }
+
+ expect(Buffer.concat(chunks).toString()).toBe("helloworld");
+});
+
+it("ReadableStream.prototype.values() cancel", async () => {
+ var cancelled = false;
+ const stream = new ReadableStream({
+ pull(controller) {
+ controller.enqueue("hello");
+ controller.enqueue("world");
+ },
+ cancel(reason) {
+ cancelled = true;
+ },
+ });
+
+ for await (const chunk of stream.values({ preventCancel: false })) {
+ break;
+ }
+ expect(cancelled).toBe(true);
+});
+
+it("ReadableStream.prototype.values() preventCancel", async () => {
+ var cancelled = false;
+ const stream = new ReadableStream({
+ pull(controller) {
+ controller.enqueue("hello");
+ controller.enqueue("world");
+ },
+ cancel(reason) {
+ cancelled = true;
+ },
+ });
+
+ for await (const chunk of stream.values({ preventCancel: true })) {
+ break;
+ }
+ expect(cancelled).toBe(false);
+});
+
+it("ReadableStream.prototype.values", async () => {
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue("hello");
+ controller.enqueue("world");
+ controller.close();
+ },
+ });
+
+ const chunks = [];
+ for await (const chunk of stream.values()) {
+ chunks.push(chunk);
+ }
+ expect(chunks.join("")).toBe("helloworld");
+});
+
+it("Bun.file() read text from pipe", async () => {
+ try {
+ unlinkSync("/tmp/fifo");
+ } catch (e) {}
+
+ console.log("here");
+ mkfifo("/tmp/fifo", 0o666);
+
+ // 65k so its less than the max on linux
+ const large = "HELLO!".repeat((((1024 * 65) / "HELLO!".length) | 0) + 1);
+
+ const chunks = [];
+
+ const proc = Bun.spawn({
+ cmd: ["bash", join(import.meta.dir + "/", "bun-streams-test-fifo.sh"), "/tmp/fifo"],
+ stderr: "inherit",
+ stdout: null,
+ stdin: null,
+ env: {
+ FIFO_TEST: large,
+ },
+ });
+ const exited = proc.exited;
+ proc.ref();
+
+ const prom = (async function () {
+ while (chunks.length === 0) {
+ var out = Bun.file("/tmp/fifo").stream();
+ for await (const chunk of out) {
+ chunks.push(chunk);
+ }
+ }
+ return Buffer.concat(chunks).toString();
+ })();
+
+ const [status, output] = await Promise.all([exited, prom]);
+ expect(output.length).toBe(large.length + 1);
+ expect(output).toBe(large + "\n");
+ expect(status).toBe(0);
+});
+
+it("exists globally", () => {
+ expect(typeof ReadableStream).toBe("function");
+ expect(typeof ReadableStreamBYOBReader).toBe("function");
+ expect(typeof ReadableStreamBYOBRequest).toBe("function");
+ expect(typeof ReadableStreamDefaultController).toBe("function");
+ expect(typeof ReadableStreamDefaultReader).toBe("function");
+ expect(typeof TransformStream).toBe("function");
+ expect(typeof TransformStreamDefaultController).toBe("function");
+ expect(typeof WritableStream).toBe("function");
+ expect(typeof WritableStreamDefaultController).toBe("function");
+ expect(typeof WritableStreamDefaultWriter).toBe("function");
+ expect(typeof ByteLengthQueuingStrategy).toBe("function");
+ expect(typeof CountQueuingStrategy).toBe("function");
+});
+
+it("new Response(stream).body", async () => {
+ var stream = new ReadableStream({
+ pull(controller) {
+ controller.enqueue("hello");
+ controller.enqueue("world");
+ controller.close();
+ },
+ cancel() {},
+ });
+ var response = new Response(stream);
+ expect(response.body).toBe(stream);
+ expect(await response.text()).toBe("helloworld");
+});
+
+it("new Request({body: stream}).body", async () => {
+ var stream = new ReadableStream({
+ pull(controller) {
+ controller.enqueue("hello");
+ controller.enqueue("world");
+ controller.close();
+ },
+ cancel() {},
+ });
+ var response = new Request({ body: stream });
+ expect(response.body).toBe(stream);
+ expect(await response.text()).toBe("helloworld");
+});
+
+it("ReadableStream (readMany)", async () => {
+ var stream = new ReadableStream({
+ pull(controller) {
+ controller.enqueue("hello");
+ controller.enqueue("world");
+ controller.close();
+ },
+ cancel() {},
+ });
+ var reader = stream.getReader();
+ const chunk = await reader.readMany();
+ expect(chunk.value.join("")).toBe("helloworld");
+ expect((await reader.read()).done).toBe(true);
+});
+
+it("ReadableStream (direct)", async () => {
+ var stream = new ReadableStream({
+ pull(controller) {
+ controller.write("hello");
+ controller.write("world");
+ controller.close();
+ },
+ cancel() {},
+ type: "direct",
+ });
+ var reader = stream.getReader();
+ const chunk = await reader.read();
+ expect(chunk.value.join("")).toBe(Buffer.from("helloworld").join(""));
+ expect((await reader.read()).done).toBe(true);
+ expect((await reader.read()).done).toBe(true);
+});
+
+it("ReadableStream (bytes)", async () => {
+ var stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(Buffer.from("abdefgh"));
+ },
+ pull(controller) {},
+ cancel() {},
+ type: "bytes",
+ });
+ const chunks = [];
+ const chunk = await stream.getReader().read();
+ chunks.push(chunk.value);
+ expect(chunks[0].join("")).toBe(Buffer.from("abdefgh").join(""));
+});
+
+it("ReadableStream (default)", async () => {
+ var stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(Buffer.from("abdefgh"));
+ controller.close();
+ },
+ pull(controller) {},
+ cancel() {},
+ });
+ const chunks = [];
+ const chunk = await stream.getReader().read();
+ chunks.push(chunk.value);
+ expect(chunks[0].join("")).toBe(Buffer.from("abdefgh").join(""));
+});
+
+it("readableStreamToArray", async () => {
+ var queue = [Buffer.from("abdefgh")];
+ var stream = new ReadableStream({
+ pull(controller) {
+ var chunk = queue.shift();
+ if (chunk) {
+ controller.enqueue(chunk);
+ } else {
+ controller.close();
+ }
+ },
+ cancel() {},
+ type: "bytes",
+ });
+
+ const chunks = await readableStreamToArray(stream);
+
+ expect(chunks[0].join("")).toBe(Buffer.from("abdefgh").join(""));
+});
+
+it("readableStreamToArrayBuffer (bytes)", async () => {
+ var queue = [Buffer.from("abdefgh")];
+ var stream = new ReadableStream({
+ pull(controller) {
+ var chunk = queue.shift();
+ if (chunk) {
+ controller.enqueue(chunk);
+ } else {
+ controller.close();
+ }
+ },
+ cancel() {},
+ type: "bytes",
+ });
+ const buffer = await readableStreamToArrayBuffer(stream);
+ expect(new TextDecoder().decode(new Uint8Array(buffer))).toBe("abdefgh");
+});
+
+it("readableStreamToArrayBuffer (default)", async () => {
+ var queue = [Buffer.from("abdefgh")];
+ var stream = new ReadableStream({
+ pull(controller) {
+ var chunk = queue.shift();
+ if (chunk) {
+ controller.enqueue(chunk);
+ } else {
+ controller.close();
+ }
+ },
+ cancel() {},
+ });
+
+ const buffer = await readableStreamToArrayBuffer(stream);
+ expect(new TextDecoder().decode(new Uint8Array(buffer))).toBe("abdefgh");
+});
+
+it("ReadableStream for Blob", async () => {
+ var blob = new Blob(["abdefgh", "ijklmnop"]);
+ expect(await blob.text()).toBe("abdefghijklmnop");
+ var stream;
+ try {
+ stream = blob.stream();
+ stream = blob.stream();
+ } catch (e) {
+ console.error(e);
+ console.error(e.stack);
+ }
+ const chunks = [];
+ var reader;
+ reader = stream.getReader();
+
+ while (true) {
+ var chunk;
+ try {
+ chunk = await reader.read();
+ } catch (e) {
+ console.error(e);
+ console.error(e.stack);
+ }
+ if (chunk.done) break;
+ chunks.push(new TextDecoder().decode(chunk.value));
+ }
+ expect(chunks.join("")).toBe(new TextDecoder().decode(Buffer.from("abdefghijklmnop")));
+});
+
+it("ReadableStream for File", async () => {
+ var blob = file(import.meta.dir + "/fetch.js.txt");
+ var stream = blob.stream();
+ const chunks = [];
+ var reader = stream.getReader();
+ stream = undefined;
+ while (true) {
+ const chunk = await reader.read();
+ if (chunk.done) break;
+ chunks.push(chunk.value);
+ }
+ reader = undefined;
+ const output = new Uint8Array(await blob.arrayBuffer()).join("");
+ const input = chunks.map(a => a.join("")).join("");
+ expect(output).toBe(input);
+});
+
+it("ReadableStream for File errors", async () => {
+ try {
+ var blob = file(import.meta.dir + "/fetch.js.txt.notfound");
+ blob.stream().getReader();
+ throw new Error("should not reach here");
+ } catch (e) {
+ expect(e.code).toBe("ENOENT");
+ expect(e.syscall).toBe("open");
+ }
+});
+
+it("ReadableStream for empty blob closes immediately", async () => {
+ var blob = new Blob([]);
+ var stream = blob.stream();
+ const chunks = [];
+ var reader = stream.getReader();
+ while (true) {
+ const chunk = await reader.read();
+ if (chunk.done) break;
+ chunks.push(chunk.value);
+ }
+
+ expect(chunks.length).toBe(0);
+});
+
+it("ReadableStream for empty file closes immediately", async () => {
+ writeFileSync("/tmp/bun-empty-file-123456", "");
+ var blob = file("/tmp/bun-empty-file-123456");
+ var stream;
+ try {
+ stream = blob.stream();
+ } catch (e) {
+ console.error(e.stack);
+ }
+ const chunks = [];
+ var reader = stream.getReader();
+ while (true) {
+ const chunk = await reader.read();
+ if (chunk.done) break;
+ chunks.push(chunk.value);
+ }
+
+ expect(chunks.length).toBe(0);
+});
+
+it("new Response(stream).arrayBuffer() (bytes)", async () => {
+ var queue = [Buffer.from("abdefgh")];
+ var stream = new ReadableStream({
+ pull(controller) {
+ var chunk = queue.shift();
+ if (chunk) {
+ controller.enqueue(chunk);
+ } else {
+ controller.close();
+ }
+ },
+ cancel() {},
+ type: "bytes",
+ });
+ const buffer = await new Response(stream).arrayBuffer();
+ expect(new TextDecoder().decode(new Uint8Array(buffer))).toBe("abdefgh");
+});
+
+it("new Response(stream).arrayBuffer() (default)", async () => {
+ var queue = [Buffer.from("abdefgh")];
+ var stream = new ReadableStream({
+ pull(controller) {
+ var chunk = queue.shift();
+ if (chunk) {
+ controller.enqueue(chunk);
+ } else {
+ controller.close();
+ }
+ },
+ cancel() {},
+ });
+ const buffer = await new Response(stream).arrayBuffer();
+ expect(new TextDecoder().decode(new Uint8Array(buffer))).toBe("abdefgh");
+});
+
+it("new Response(stream).text() (default)", async () => {
+ var queue = [Buffer.from("abdefgh")];
+ var stream = new ReadableStream({
+ pull(controller) {
+ var chunk = queue.shift();
+ if (chunk) {
+ controller.enqueue(chunk);
+ } else {
+ controller.close();
+ }
+ },
+ cancel() {},
+ });
+ const text = await new Response(stream).text();
+ expect(text).toBe("abdefgh");
+});
+
+it("new Response(stream).json() (default)", async () => {
+ var queue = [Buffer.from(JSON.stringify({ hello: true }))];
+ var stream = new ReadableStream({
+ pull(controller) {
+ var chunk = queue.shift();
+ if (chunk) {
+ controller.enqueue(chunk);
+ } else {
+ controller.close();
+ }
+ },
+ cancel() {},
+ });
+ const json = await new Response(stream).json();
+ expect(json.hello).toBe(true);
+});
+
+it("new Response(stream).blob() (default)", async () => {
+ var queue = [Buffer.from(JSON.stringify({ hello: true }))];
+ var stream = new ReadableStream({
+ pull(controller) {
+ var chunk = queue.shift();
+ if (chunk) {
+ controller.enqueue(chunk);
+ } else {
+ controller.close();
+ }
+ },
+ cancel() {},
+ });
+ const response = new Response(stream);
+ const blob = await response.blob();
+ expect(await blob.text()).toBe('{"hello":true}');
+});
+
+it("Blob.stream() -> new Response(stream).text()", async () => {
+ var blob = new Blob(["abdefgh"]);
+ var stream = blob.stream();
+ const text = await new Response(stream).text();
+ expect(text).toBe("abdefgh");
+});
+
+it("Bun.file().stream() read text from large file", async () => {
+ const hugely = "HELLO!".repeat(1024 * 1024 * 10);
+ const tmpfile = join(realpathSync(tmpdir()), "bun-streams-test.txt");
+ writeFileSync(tmpfile, hugely);
+ try {
+ const chunks = [];
+ for await (const chunk of Bun.file(tmpfile).stream()) {
+ chunks.push(chunk);
+ }
+ const output = Buffer.concat(chunks).toString();
+ expect(output).toHaveLength(hugely.length);
+ expect(output).toBe(hugely);
+ } finally {
+ unlinkSync(tmpfile);
+ }
+});
diff --git a/test/js/web/timers/microtask.test.js b/test/js/web/timers/microtask.test.js
new file mode 100644
index 000000000..f41159cfa
--- /dev/null
+++ b/test/js/web/timers/microtask.test.js
@@ -0,0 +1,74 @@
+import { it } from "bun:test";
+
+it("queueMicrotask", async () => {
+ // You can verify this test is correct by copy pasting this into a browser's console and checking it doesn't throw an error.
+ var run = 0;
+
+ await new Promise((resolve, reject) => {
+ queueMicrotask(() => {
+ if (run++ != 0) {
+ reject(new Error("Microtask execution order is wrong: " + run));
+ }
+ queueMicrotask(() => {
+ if (run++ != 3) {
+ reject(new Error("Microtask execution order is wrong: " + run));
+ }
+ });
+ });
+ queueMicrotask(() => {
+ if (run++ != 1) {
+ reject(new Error("Microtask execution order is wrong: " + run));
+ }
+ queueMicrotask(() => {
+ if (run++ != 4) {
+ reject(new Error("Microtask execution order is wrong: " + run));
+ }
+
+ queueMicrotask(() => {
+ if (run++ != 6) {
+ reject(new Error("Microtask execution order is wrong: " + run));
+ }
+ });
+ });
+ });
+ queueMicrotask(() => {
+ if (run++ != 2) {
+ reject(new Error("Microtask execution order is wrong: " + run));
+ }
+ queueMicrotask(() => {
+ if (run++ != 5) {
+ reject(new Error("Microtask execution order is wrong: " + run));
+ }
+
+ queueMicrotask(() => {
+ if (run++ != 7) {
+ reject(new Error("Microtask execution order is wrong: " + run));
+ }
+ resolve(true);
+ });
+ });
+ });
+ });
+
+ {
+ var passed = false;
+ try {
+ queueMicrotask(1234);
+ } catch (exception) {
+ passed = exception instanceof TypeError;
+ }
+
+ if (!passed) throw new Error("queueMicrotask should throw a TypeError if the argument is not a function");
+ }
+
+ {
+ var passed = false;
+ try {
+ queueMicrotask();
+ } catch (exception) {
+ passed = exception instanceof TypeError;
+ }
+
+ if (!passed) throw new Error("queueMicrotask should throw a TypeError if the argument is empty");
+ }
+});
diff --git a/test/js/web/timers/performance.test.js b/test/js/web/timers/performance.test.js
new file mode 100644
index 000000000..dd50c4dc6
--- /dev/null
+++ b/test/js/web/timers/performance.test.js
@@ -0,0 +1,22 @@
+import { expect, it } from "bun:test";
+
+it("performance.now() should be monotonic", () => {
+ const first = performance.now();
+ const second = performance.now();
+ const third = performance.now();
+ const fourth = performance.now();
+ const fifth = performance.now();
+ const sixth = performance.now();
+ expect(first < second).toBe(true);
+ expect(second < third).toBe(true);
+ expect(third < fourth).toBe(true);
+ expect(fourth < fifth).toBe(true);
+ expect(fifth < sixth).toBe(true);
+ expect(Bun.nanoseconds() > 0).toBe(true);
+ expect(Bun.nanoseconds() > sixth).toBe(true);
+ expect(typeof Bun.nanoseconds() === "number").toBe(true);
+});
+
+it("performance.timeOrigin + performance.now() should be similar to Date.now()", () => {
+ expect(Math.abs(performance.timeOrigin + performance.now() - Date.now()) < 1000).toBe(true);
+});
diff --git a/test/js/web/timers/setImmediate.test.js b/test/js/web/timers/setImmediate.test.js
new file mode 100644
index 000000000..9cd6fa1c9
--- /dev/null
+++ b/test/js/web/timers/setImmediate.test.js
@@ -0,0 +1,47 @@
+import { it, expect } from "bun:test";
+
+it("setImmediate", async () => {
+ var lastID = -1;
+ const result = await new Promise((resolve, reject) => {
+ var numbers = [];
+
+ for (let i = 0; i < 10; i++) {
+ const id = setImmediate((...args) => {
+ numbers.push(i);
+ if (i === 9) {
+ resolve(numbers);
+ }
+ try {
+ expect(args.length).toBe(1);
+ expect(args[0]).toBe(i);
+ } catch (err) {
+ reject(err);
+ }
+ }, i);
+ expect(id > lastID).toBe(true);
+ lastID = id;
+ }
+ });
+
+ for (let j = 0; j < result.length; j++) {
+ expect(result[j]).toBe(j);
+ }
+ expect(result.length).toBe(10);
+});
+
+it("clearImmediate", async () => {
+ var called = false;
+ const id = setImmediate(() => {
+ called = true;
+ expect(false).toBe(true);
+ });
+ clearImmediate(id);
+
+ // assert it doesn't crash if you call clearImmediate twice
+ clearImmediate(id);
+
+ await new Promise((resolve, reject) => {
+ setImmediate(resolve);
+ });
+ expect(called).toBe(false);
+});
diff --git a/test/js/web/timers/setInterval.test.js b/test/js/web/timers/setInterval.test.js
new file mode 100644
index 000000000..7b03afba5
--- /dev/null
+++ b/test/js/web/timers/setInterval.test.js
@@ -0,0 +1,61 @@
+import { it, expect } from "bun:test";
+
+it("setInterval", async () => {
+ var counter = 0;
+ var start;
+ const result = await new Promise((resolve, reject) => {
+ start = performance.now();
+
+ var id = setInterval(
+ (...args) => {
+ counter++;
+ if (counter === 10) {
+ resolve(counter);
+ clearInterval(id);
+ }
+ try {
+ expect(args).toStrictEqual(["foo"]);
+ } catch (err) {
+ reject(err);
+ clearInterval(id);
+ }
+ },
+ 1,
+ "foo",
+ );
+ });
+
+ expect(result).toBe(10);
+ expect(performance.now() - start >= 10).toBe(true);
+});
+
+it("clearInterval", async () => {
+ var called = false;
+ const id = setInterval(() => {
+ called = true;
+ expect(false).toBe(true);
+ }, 1);
+ clearInterval(id);
+ await new Promise((resolve, reject) => {
+ setInterval(() => {
+ resolve();
+ }, 10);
+ });
+ expect(called).toBe(false);
+});
+
+it("async setInterval", async () => {
+ var remaining = 5;
+ await new Promise((resolve, reject) => {
+ queueMicrotask(() => {
+ var id = setInterval(async () => {
+ await 1;
+ remaining--;
+ if (remaining === 0) {
+ clearInterval(id);
+ resolve();
+ }
+ }, 1);
+ });
+ });
+});
diff --git a/test/js/web/timers/setTimeout.test.js b/test/js/web/timers/setTimeout.test.js
new file mode 100644
index 000000000..88472adc7
--- /dev/null
+++ b/test/js/web/timers/setTimeout.test.js
@@ -0,0 +1,173 @@
+import { it, expect } from "bun:test";
+
+it("setTimeout", async () => {
+ var lastID = -1;
+ const result = await new Promise((resolve, reject) => {
+ var numbers = [];
+
+ for (let i = 0; i < 10; i++) {
+ const id = setTimeout(
+ (...args) => {
+ numbers.push(i);
+ if (i === 9) {
+ resolve(numbers);
+ }
+ try {
+ expect(args).toStrictEqual(["foo"]);
+ } catch (err) {
+ reject(err);
+ }
+ },
+ i,
+ "foo",
+ );
+ expect(+id > lastID).toBe(true);
+ lastID = id;
+ }
+ });
+
+ for (let j = 0; j < result.length; j++) {
+ expect(result[j]).toBe(j);
+ }
+ expect(result.length).toBe(10);
+});
+
+it("clearTimeout", async () => {
+ var called = false;
+
+ // as object
+ {
+ const id = setTimeout(() => {
+ called = true;
+ expect(false).toBe(true);
+ }, 0);
+ clearTimeout(id);
+
+ // assert it doesn't crash if you call clearTimeout twice
+ clearTimeout(id);
+ }
+
+ // as number
+ {
+ const id = setTimeout(() => {
+ called = true;
+ expect(false).toBe(true);
+ }, 0);
+ clearTimeout(+id);
+
+ // assert it doesn't crash if you call clearTimeout twice
+ clearTimeout(+id);
+ }
+
+ await new Promise((resolve, reject) => {
+ setTimeout(resolve, 10);
+ });
+ expect(called).toBe(false);
+});
+
+it("setTimeout(() => {}, 0)", async () => {
+ var called = false;
+ setTimeout(() => {
+ called = true;
+ }, 0);
+ await new Promise((resolve, reject) => {
+ setTimeout(() => {
+ resolve();
+ }, 10);
+ });
+ expect(called).toBe(true);
+ var ranFirst = -1;
+ setTimeout(() => {
+ if (ranFirst === -1) ranFirst = 1;
+ }, 1);
+ setTimeout(() => {
+ if (ranFirst === -1) ranFirst = 0;
+ }, 0);
+
+ await new Promise((resolve, reject) => {
+ setTimeout(() => {
+ resolve();
+ }, 10);
+ });
+ expect(ranFirst).toBe(0);
+
+ ranFirst = -1;
+
+ const id = setTimeout(() => {
+ ranFirst = 0;
+ }, 0);
+ clearTimeout(id);
+ await new Promise((resolve, reject) => {
+ setTimeout(() => {
+ resolve();
+ }, 10);
+ });
+ expect(ranFirst).toBe(-1);
+});
+
+it("Bun.sleep", async () => {
+ var sleeps = 0;
+ await Bun.sleep(0);
+ const start = performance.now();
+ sleeps++;
+ await Bun.sleep(1);
+ sleeps++;
+ await Bun.sleep(2);
+ sleeps++;
+ const end = performance.now();
+ expect((end - start) * 1000).toBeGreaterThanOrEqual(3);
+
+ expect(sleeps).toBe(3);
+});
+
+it("Bun.sleep propagates exceptions", async () => {
+ try {
+ await Bun.sleep(1).then(a => {
+ throw new Error("TestPassed");
+ });
+ throw "Should not reach here";
+ } catch (err) {
+ expect(err.message).toBe("TestPassed");
+ }
+});
+
+it("Bun.sleep works with a Date object", async () => {
+ var ten_ms = new Date();
+ ten_ms.setMilliseconds(ten_ms.getMilliseconds() + 12);
+ const now = performance.now();
+ await Bun.sleep(ten_ms);
+ expect(performance.now() - now).toBeGreaterThanOrEqual(10);
+});
+
+it("node.js timers/promises setTimeout propagates exceptions", async () => {
+ const { setTimeout } = require("timers/promises");
+ try {
+ await setTimeout(1).then(a => {
+ throw new Error("TestPassed");
+ });
+ throw "Should not reach here";
+ } catch (err) {
+ expect(err.message).toBe("TestPassed");
+ }
+});
+
+it.skip("order of setTimeouts", done => {
+ var nums = [];
+ var maybeDone = cb => {
+ return () => {
+ cb();
+ if (nums.length === 4) {
+ try {
+ expect(nums).toEqual([1, 2, 3, 4]);
+ done();
+ } catch (e) {
+ done(e);
+ }
+ }
+ };
+ };
+ setTimeout(maybeDone(() => nums.push(2)));
+ setTimeout(maybeDone(() => nums.push(3), 0));
+ setTimeout(maybeDone(() => nums.push(4), 1));
+ Promise.resolve().then(maybeDone(() => nums.push(1)));
+});
diff --git a/test/js/web/url/url.test.ts b/test/js/web/url/url.test.ts
new file mode 100644
index 000000000..19e10b262
--- /dev/null
+++ b/test/js/web/url/url.test.ts
@@ -0,0 +1,137 @@
+import { describe, it, expect } from "bun:test";
+
+describe("url", () => {
+ it("prints", () => {
+ expect(Bun.inspect(new URL("https://example.com"))).toBe(`URL {
+ href: "https://example.com/",
+ origin: "https://example.com",
+ protocol: "https:",
+ username: "",
+ password: "",
+ host: "example.com",
+ hostname: "example.com",
+ port: "",
+ pathname: "/",
+ hash: "",
+ search: "",
+ searchParams: URLSearchParams {
+ append: [Function: append],
+ delete: [Function: delete],
+ get: [Function: get],
+ getAll: [Function: getAll],
+ has: [Function: has],
+ set: [Function: set],
+ sort: [Function: sort],
+ entries: [Function: entries],
+ keys: [Function: keys],
+ values: [Function: values],
+ forEach: [Function: forEach],
+ toString: [Function: toString],
+ [Symbol(Symbol.iterator)]: [Function: entries]
+ },
+ toJSON: [Function: toJSON],
+ toString: [Function: toString]
+}`);
+
+ expect(
+ Bun.inspect(
+ new URL("https://github.com/oven-sh/bun/issues/135?hello%20i%20have%20spaces%20thank%20you%20good%20night"),
+ ),
+ ).toBe(`URL {
+ href: "https://github.com/oven-sh/bun/issues/135?hello%20i%20have%20spaces%20thank%20you%20good%20night",
+ origin: "https://github.com",
+ protocol: "https:",
+ username: "",
+ password: "",
+ host: "github.com",
+ hostname: "github.com",
+ port: "",
+ pathname: "/oven-sh/bun/issues/135",
+ hash: "",
+ search: "?hello%20i%20have%20spaces%20thank%20you%20good%20night",
+ searchParams: URLSearchParams {
+ append: [Function: append],
+ delete: [Function: delete],
+ get: [Function: get],
+ getAll: [Function: getAll],
+ has: [Function: has],
+ set: [Function: set],
+ sort: [Function: sort],
+ entries: [Function: entries],
+ keys: [Function: keys],
+ values: [Function: values],
+ forEach: [Function: forEach],
+ toString: [Function: toString],
+ [Symbol(Symbol.iterator)]: [Function: entries]
+ },
+ toJSON: [Function: toJSON],
+ toString: [Function: toString]
+}`);
+ });
+ it("works", () => {
+ const inputs = [
+ [
+ "https://username:password@api.foo.bar.com:9999/baz/okay/i/123?ran=out&of=things#to-use-as-a-placeholder",
+ {
+ hash: "#to-use-as-a-placeholder",
+ host: "api.foo.bar.com:9999",
+ hostname: "api.foo.bar.com",
+ href: "https://username:password@api.foo.bar.com:9999/baz/okay/i/123?ran=out&of=things#to-use-as-a-placeholder",
+ origin: "https://api.foo.bar.com:9999",
+ password: "password",
+ pathname: "/baz/okay/i/123",
+ port: "9999",
+ protocol: "https:",
+ search: "?ran=out&of=things",
+ username: "username",
+ },
+ ],
+ [
+ "https://url.spec.whatwg.org/#url-serializing",
+ {
+ hash: "#url-serializing",
+ host: "url.spec.whatwg.org",
+ hostname: "url.spec.whatwg.org",
+ href: "https://url.spec.whatwg.org/#url-serializing",
+ origin: "https://url.spec.whatwg.org",
+ password: "",
+ pathname: "/",
+ port: "",
+ protocol: "https:",
+ search: "",
+ username: "",
+ },
+ ],
+ [
+ "https://url.spec.whatwg.org#url-serializing",
+ {
+ hash: "#url-serializing",
+ host: "url.spec.whatwg.org",
+ hostname: "url.spec.whatwg.org",
+ href: "https://url.spec.whatwg.org/#url-serializing",
+ origin: "https://url.spec.whatwg.org",
+ password: "",
+ pathname: "/",
+ port: "",
+ protocol: "https:",
+ search: "",
+ username: "",
+ },
+ ],
+ ] as const;
+
+ for (let [url, values] of inputs) {
+ const result = new URL(url);
+ expect(result.hash).toBe(values.hash);
+ expect(result.host).toBe(values.host);
+ expect(result.hostname).toBe(values.hostname);
+ expect(result.href).toBe(values.href);
+ expect(result.password).toBe(values.password);
+ expect(result.pathname).toBe(values.pathname);
+ expect(result.port).toBe(values.port);
+ expect(result.protocol).toBe(values.protocol);
+ expect(result.search).toBe(values.search);
+ expect(result.username).toBe(values.username);
+ }
+ });
+});
diff --git a/test/js/web/util/atob.test.js b/test/js/web/util/atob.test.js
new file mode 100644
index 000000000..4945829e1
--- /dev/null
+++ b/test/js/web/util/atob.test.js
@@ -0,0 +1,77 @@
+import { expect, it } from "bun:test";
+
+function expectInvalidCharacters(val) {
+ try {
+ atob(val);
+ throw new Error("Expected error");
+ } catch (error) {
+ expect(error.message).toBe("The string contains invalid characters.");
+ }
+}
+
+it("atob", () => {
+ expect(atob("YQ==")).toBe("a");
+ expect(atob("YWI=")).toBe("ab");
+ expect(atob("YWJj")).toBe("abc");
+ expect(atob("YWJjZA==")).toBe("abcd");
+ expect(atob("YWJjZGU=")).toBe("abcde");
+ expect(atob("YWJjZGVm")).toBe("abcdef");
+ expect(atob("zzzz")).toBe("Ï<ó");
+ expect(atob("")).toBe("");
+ expect(atob(null)).toBe("žée");
+ expect(atob("6ek=")).toBe("éé");
+ expect(atob("6ek")).toBe("éé");
+ expect(atob("gIE=")).toBe("€");
+ expect(atob("zz")).toBe("Ï");
+ expect(atob("zzz")).toBe("Ï<");
+ expect(atob("zzz=")).toBe("Ï<");
+ expect(atob(" YQ==")).toBe("a");
+ expect(atob("YQ==\u000a")).toBe("a");
+
+ try {
+ atob();
+ } catch (error) {
+ expect(error.name).toBe("TypeError");
+ }
+ expectInvalidCharacters(undefined);
+ expectInvalidCharacters(" abcd===");
+ expectInvalidCharacters("abcd=== ");
+ expectInvalidCharacters("abcd ===");
+ expectInvalidCharacters("тест");
+ expectInvalidCharacters("z");
+ expectInvalidCharacters("zzz==");
+ expectInvalidCharacters("zzz===");
+ expectInvalidCharacters("zzz====");
+ expectInvalidCharacters("zzz=====");
+ expectInvalidCharacters("zzzzz");
+ expectInvalidCharacters("z=zz");
+ expectInvalidCharacters("=");
+ expectInvalidCharacters("==");
+ expectInvalidCharacters("===");
+ expectInvalidCharacters("====");
+ expectInvalidCharacters("=====");
+});
+
+it("btoa", () => {
+ expect(btoa("a")).toBe("YQ==");
+ expect(btoa("ab")).toBe("YWI=");
+ expect(btoa("abc")).toBe("YWJj");
+ expect(btoa("abcd")).toBe("YWJjZA==");
+ expect(btoa("abcde")).toBe("YWJjZGU=");
+ expect(btoa("abcdef")).toBe("YWJjZGVm");
+ expect(typeof btoa).toBe("function");
+ try {
+ btoa();
+ throw new Error("Expected error");
+ } catch (error) {
+ expect(error.name).toBe("TypeError");
+ }
+ var window = "[object Window]";
+ expect(btoa("")).toBe("");
+ expect(btoa(null)).toBe("bnVsbA==");
+ expect(btoa(undefined)).toBe("dW5kZWZpbmVk");
+ expect(btoa(window)).toBe("W29iamVjdCBXaW5kb3dd");
+ expect(btoa("éé")).toBe("6ek=");
+ expect(btoa("\u0080\u0081")).toBe("gIE=");
+ expect(btoa(Bun)).toBe(btoa("[object Bun]"));
+});
diff --git a/test/js/web/web-globals.test.js b/test/js/web/web-globals.test.js
new file mode 100644
index 000000000..b7a243190
--- /dev/null
+++ b/test/js/web/web-globals.test.js
@@ -0,0 +1,156 @@
+import { unsafe } from "bun";
+import { expect, it, test } from "bun:test";
+import { withoutAggressiveGC } from "harness";
+
+test("exists", () => {
+ expect(typeof URL !== "undefined").toBe(true);
+ expect(typeof URLSearchParams !== "undefined").toBe(true);
+ expect(typeof DOMException !== "undefined").toBe(true);
+ expect(typeof Event !== "undefined").toBe(true);
+ expect(typeof EventTarget !== "undefined").toBe(true);
+ expect(typeof AbortController !== "undefined").toBe(true);
+ expect(typeof AbortSignal !== "undefined").toBe(true);
+ expect(typeof CustomEvent !== "undefined").toBe(true);
+ expect(typeof Headers !== "undefined").toBe(true);
+ expect(typeof ErrorEvent !== "undefined").toBe(true);
+ expect(typeof CloseEvent !== "undefined").toBe(true);
+ expect(typeof MessageEvent !== "undefined").toBe(true);
+ expect(typeof TextEncoder !== "undefined").toBe(true);
+ expect(typeof WebSocket !== "undefined").toBe(true);
+ expect(typeof Blob !== "undefined").toBe(true);
+ expect(typeof FormData !== "undefined").toBe(true);
+});
+
+test("CloseEvent", () => {
+ var event = new CloseEvent("close", { reason: "world" });
+ expect(event.type).toBe("close");
+ const target = new EventTarget();
+ var called = false;
+ target.addEventListener("close", ({ type, reason }) => {
+ expect(type).toBe("close");
+ expect(reason).toBe("world");
+ called = true;
+ });
+ target.dispatchEvent(event);
+ expect(called).toBe(true);
+});
+
+test("MessageEvent", () => {
+ var event = new MessageEvent("message", { data: "world" });
+ expect(event.type).toBe("message");
+ const target = new EventTarget();
+ var called = false;
+ target.addEventListener("message", ({ type, data }) => {
+ expect(type).toBe("message");
+ expect(data).toBe("world");
+ called = true;
+ });
+ target.dispatchEvent(event);
+ expect(called).toBe(true);
+});
+
+it("crypto.getRandomValues", () => {
+ var foo = new Uint8Array(32);
+
+ // run it once buffered and unbuffered
+ {
+ var array = crypto.getRandomValues(foo);
+ expect(array).toBe(foo);
+ expect(array.reduce((sum, a) => (sum += a === 0), 0) != foo.length).toBe(true);
+ }
+
+ // disable it for this block because it tends to get stuck here running the GC forever
+ withoutAggressiveGC(() => {
+ // run it again to check that the fast path works
+ for (var i = 0; i < 9000; i++) {
+ var array = crypto.getRandomValues(foo);
+ expect(array).toBe(foo);
+ }
+ });
+
+ // run it on a large input
+ expect(!!crypto.getRandomValues(new Uint8Array(8096)).find(a => a > 0)).toBe(true);
+
+ {
+ // any additional input into getRandomValues() makes it unbuffered
+ var array = crypto.getRandomValues(foo, "unbuffered");
+ expect(array).toBe(foo);
+ expect(array.reduce((sum, a) => (sum += a === 0), 0) != foo.length).toBe(true);
+ }
+});
+
+// not actually a web global
+it("crypto.timingSafeEqual", () => {
+ const crypto = import.meta.require("node:crypto");
+ var uuidStr = crypto.randomUUID();
+ expect(uuidStr.length).toBe(36);
+ expect(uuidStr[8]).toBe("-");
+ expect(uuidStr[13]).toBe("-");
+ expect(uuidStr[18]).toBe("-");
+ expect(uuidStr[23]).toBe("-");
+ const uuid = Buffer.from(uuidStr);
+
+ expect(crypto.timingSafeEqual(uuid, uuid)).toBe(true);
+ expect(crypto.timingSafeEqual(uuid, uuid.slice())).toBe(true);
+ try {
+ crypto.timingSafeEqual(uuid, uuid.slice(1));
+ expect(false).toBe(true);
+ } catch (e) {}
+
+ try {
+ crypto.timingSafeEqual(uuid, uuid.slice(0, uuid.length - 2));
+ expect(false).toBe(true);
+ } catch (e) {
+ expect(e.message).toBe("Input buffers must have the same length");
+ }
+
+ try {
+ expect(crypto.timingSafeEqual(uuid, crypto.randomUUID())).toBe(false);
+ expect(false).toBe(true);
+ } catch (e) {
+ expect(e.name).toBe("TypeError");
+ }
+
+ var shorter = uuid.slice(0, 1);
+ for (let i = 0; i < 9000; i++) {
+ if (!crypto.timingSafeEqual(shorter, shorter)) throw new Error("fail");
+ }
+});
+
+it("crypto.randomUUID", () => {
+ var uuid = crypto.randomUUID();
+ expect(uuid.length).toBe(36);
+ expect(uuid[8]).toBe("-");
+ expect(uuid[13]).toBe("-");
+ expect(uuid[18]).toBe("-");
+ expect(uuid[23]).toBe("-");
+
+ withoutAggressiveGC(() => {
+ // check that the fast path works
+ for (let i = 0; i < 9000; i++) {
+ var uuid2 = crypto.randomUUID();
+ expect(uuid2.length).toBe(36);
+ expect(uuid2[8]).toBe("-");
+ expect(uuid2[13]).toBe("-");
+ expect(uuid2[18]).toBe("-");
+ expect(uuid2[23]).toBe("-");
+ }
+ });
+});
+
+it("URL.prototype.origin", () => {
+ const url = new URL("https://html.spec.whatwg.org/");
+ const { origin, host, hostname } = url;
+
+ expect(hostname).toBe("html.spec.whatwg.org");
+ expect(host).toBe("html.spec.whatwg.org");
+ expect(origin).toBe("https://html.spec.whatwg.org");
+});
+
+test("navigator", () => {
+ expect(globalThis.navigator !== undefined).toBe(true);
+ const version = process.versions.bun;
+ const userAgent = `Bun/${version}`;
+ expect(navigator.hardwareConcurrency > 0).toBe(true);
+ expect(navigator.userAgent).toBe(userAgent);
+});
diff --git a/test/js/web/websocket/websocket-subprocess.ts b/test/js/web/websocket/websocket-subprocess.ts
new file mode 100644
index 000000000..fd25b7fd5
--- /dev/null
+++ b/test/js/web/websocket/websocket-subprocess.ts
@@ -0,0 +1,13 @@
+const host = process.argv[2];
+
+const ws = new WebSocket(host);
+
+ws.onmessage = message => {
+ if (message.data === "hello websocket") {
+ ws.send("hello");
+ } else if (message.data === "timeout") {
+ setTimeout(() => {
+ ws.send("close");
+ }, 300);
+ }
+};
diff --git a/test/js/web/websocket/websocket.test.js b/test/js/web/websocket/websocket.test.js
new file mode 100644
index 000000000..f0f29c1c3
--- /dev/null
+++ b/test/js/web/websocket/websocket.test.js
@@ -0,0 +1,263 @@
+import { describe, it, expect } from "bun:test";
+import { unsafe, spawn, readableStreamToText } from "bun";
+import { bunExe, bunEnv, gc } from "harness";
+
+const TEST_WEBSOCKET_HOST = process.env.TEST_WEBSOCKET_HOST || "wss://ws.postman-echo.com/raw";
+
+describe("WebSocket", () => {
+ it("should connect", async () => {
+ const ws = new WebSocket(TEST_WEBSOCKET_HOST);
+ await new Promise((resolve, reject) => {
+ ws.onopen = resolve;
+ ws.onerror = reject;
+ });
+ var closed = new Promise((resolve, reject) => {
+ ws.onclose = resolve;
+ });
+ ws.close();
+ await closed;
+ });
+
+ it("should connect over https", async () => {
+ const ws = new WebSocket(TEST_WEBSOCKET_HOST.replaceAll("wss:", "https:"));
+ await new Promise((resolve, reject) => {
+ ws.onopen = resolve;
+ ws.onerror = reject;
+ });
+ var closed = new Promise((resolve, reject) => {
+ ws.onclose = resolve;
+ });
+ ws.close();
+ await closed;
+ });
+
+ it("supports headers", done => {
+ const server = Bun.serve({
+ port: 8024,
+ fetch(req, server) {
+ expect(req.headers.get("X-Hello")).toBe("World");
+ expect(req.headers.get("content-type")).toBe("lolwut");
+ server.stop();
+ done();
+ return new Response();
+ },
+ websocket: {
+ open(ws) {
+ ws.close();
+ },
+ },
+ });
+ const ws = new WebSocket(`ws://${server.hostname}:${server.port}`, {
+ headers: {
+ "X-Hello": "World",
+ "content-type": "lolwut",
+ },
+ });
+ });
+
+ it("should connect over http", done => {
+ const server = Bun.serve({
+ port: 8025,
+ fetch(req, server) {
+ server.stop();
+ done();
+ return new Response();
+ },
+ websocket: {
+ open(ws) {
+ ws.close();
+ },
+ },
+ });
+ const ws = new WebSocket(`http://${server.hostname}:${server.port}`, {});
+ });
+
+ it("should send and receive messages", async () => {
+ const ws = new WebSocket(TEST_WEBSOCKET_HOST);
+ await new Promise((resolve, reject) => {
+ ws.onopen = resolve;
+ ws.onerror = reject;
+ ws.onclose = () => {
+ reject("WebSocket closed");
+ };
+ });
+ const count = 10;
+
+ // 10 messages in burst
+ var promise = new Promise((resolve, reject) => {
+ var remain = count;
+ ws.onmessage = event => {
+ gc(true);
+ expect(event.data).toBe("Hello World!");
+ remain--;
+
+ if (remain <= 0) {
+ ws.onmessage = () => {};
+ resolve();
+ }
+ };
+ ws.onerror = reject;
+ });
+
+ for (let i = 0; i < count; i++) {
+ ws.send("Hello World!");
+ gc(true);
+ }
+
+ await promise;
+ var echo = 0;
+
+ // 10 messages one at a time
+ function waitForEcho() {
+ return new Promise((resolve, reject) => {
+ gc(true);
+ const msg = `Hello World! ${echo++}`;
+ ws.onmessage = event => {
+ expect(event.data).toBe(msg);
+ resolve();
+ };
+ ws.onerror = reject;
+ ws.onclose = reject;
+ ws.send(msg);
+ gc(true);
+ });
+ }
+ gc(true);
+ for (let i = 0; i < count; i++) await waitForEcho();
+ ws.onclose = () => {};
+ ws.onerror = () => {};
+ ws.close();
+ gc(true);
+ });
+});
+
+describe("websocket in subprocess", () => {
+ var port = 8765;
+ it("should exit", async () => {
+ let messageReceived = false;
+ const server = Bun.serve({
+ port: port++,
+ fetch(req, server) {
+ if (server.upgrade(req)) {
+ return;
+ }
+
+ return new Response("http response");
+ },
+ websocket: {
+ open(ws) {
+ ws.send("hello websocket");
+ },
+ message(ws) {
+ messageReceived = true;
+ ws.close();
+ },
+ close(ws) {},
+ },
+ });
+ const subprocess = Bun.spawn({
+ cmd: [bunExe(), import.meta.dir + "/websocket-subprocess.ts", `http://${server.hostname}:${server.port}`],
+ stderr: "pipe",
+ stdin: "pipe",
+ stdout: "pipe",
+ env: bunEnv,
+ });
+
+ expect(await subprocess.exited).toBe(0);
+ expect(messageReceived).toBe(true);
+ server.stop(true);
+ });
+
+ it("should exit after killed", async () => {
+ const subprocess = Bun.spawn({
+ cmd: [bunExe(), import.meta.dir + "/websocket-subprocess.ts", TEST_WEBSOCKET_HOST],
+ stderr: "pipe",
+ stdin: "pipe",
+ stdout: "pipe",
+ env: bunEnv,
+ });
+
+ subprocess.kill();
+
+ expect(await subprocess.exited).toBe("SIGHUP");
+ });
+
+ it("should exit with invalid url", async () => {
+ const subprocess = Bun.spawn({
+ cmd: [bunExe(), import.meta.dir + "/websocket-subprocess.ts", "invalid url"],
+ stderr: "pipe",
+ stdin: "pipe",
+ stdout: "pipe",
+ env: bunEnv,
+ });
+
+ expect(await subprocess.exited).toBe(1);
+ });
+
+ it("should exit after timeout", async () => {
+ let messageReceived = false;
+ let start = 0;
+ const server = Bun.serve({
+ port: port++,
+ fetch(req, server) {
+ if (server.upgrade(req)) {
+ return;
+ }
+
+ return new Response("http response");
+ },
+ websocket: {
+ open(ws) {
+ start = performance.now();
+ ws.send("timeout");
+ },
+ message(ws, message) {
+ messageReceived = true;
+ expect(performance.now() - start >= 300).toBe(true);
+ ws.close();
+ },
+ close(ws) {},
+ },
+ });
+ const subprocess = Bun.spawn({
+ cmd: [bunExe(), import.meta.dir + "/websocket-subprocess.ts", `http://${server.hostname}:${server.port}`],
+ stderr: "pipe",
+ stdin: "pipe",
+ stdout: "pipe",
+ env: bunEnv,
+ });
+
+ expect(await subprocess.exited).toBe(0);
+ expect(messageReceived).toBe(true);
+ server.stop(true);
+ });
+
+ it("should exit after server stop and 0 messages", async () => {
+ const server = Bun.serve({
+ port: port++,
+ fetch(req, server) {
+ if (server.upgrade(req)) {
+ return;
+ }
+
+ return new Response("http response");
+ },
+ websocket: {
+ open(ws) {},
+ message(ws, message) {},
+ close(ws) {},
+ },
+ });
+
+ const subprocess = Bun.spawn({
+ cmd: [bunExe(), import.meta.dir + "/websocket-subprocess.ts", `http://${server.hostname}:${server.port}`],
+ stderr: "pipe",
+ stdin: "pipe",
+ stdout: "pipe",
+ env: bunEnv,
+ });
+
+ server.stop(true);
+ expect(await subprocess.exited).toBe(0);
+ });
+});