@sproutboat/runtime 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { ensurePorffor } from "../../toolchain/src/acquire";
6
+ import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
7
+
8
+ // URL as WHATWG defines it (Bun's URL is the reference): base URLs, setters on every
9
+ // part, hash, default ports, dot segments, percent-encoding, and searchParams writing
10
+ // back into the URL. Each case is an expression over U, the URL class under test.
11
+ const CASES = [
12
+ `new U("https://Example.COM:443/a/./b/../c?x=1#frag").href`,
13
+ `new U("http://example.com:80").href`,
14
+ `new U("http://example.com:8080/p").host`,
15
+ `new U("/path?q=1", "https://a.com/base/x").href`,
16
+ `new U("rel", "https://a.com/base/x").href`,
17
+ `new U("../up", "https://a.com/a/b/c").href`,
18
+ `new U("?q", "https://a.com/p?old#h").href`,
19
+ `new U("#h2", "https://a.com/p?old#h").href`,
20
+ `new U("//other.com/x", "https://a.com/").href`,
21
+ `new U("./", "https://a.com/a/b").href`,
22
+ `new U("..", "https://a.com/a/b/").href`,
23
+ `new U("x", new U("https://a.com/d/")).href`,
24
+ `(() => { const u = new U("https://foo.example.com/api.js"); u.hostname = "example.com"; return u.href; })()`,
25
+ `(() => { const u = new U("https://a.com/"); u.port = "8080"; return u.href; })()`,
26
+ `(() => { const u = new U("https://a.com:8080/"); u.port = "443"; return u.href; })()`,
27
+ `(() => { const u = new U("https://a.com:8080/"); u.port = ""; return u.href; })()`,
28
+ `(() => { const u = new U("https://a.com/x"); u.protocol = "http"; return u.href; })()`,
29
+ `(() => { const u = new U("https://a.com/x"); u.protocol = "http:"; u.port = "80"; return u.href; })()`,
30
+ `(() => { const u = new U("https://a.com/x"); u.host = "b.com:9"; return u.href; })()`,
31
+ `(() => { const u = new U("https://a.com/x"); u.search = "a=1&b=2"; return u.href; })()`,
32
+ `(() => { const u = new U("https://a.com/x?a=1"); u.search = ""; return u.href; })()`,
33
+ `(() => { const u = new U("https://a.com/x"); u.hash = "top"; return u.href + " " + u.hash; })()`,
34
+ `(() => { const u = new U("https://a.com/x#top"); u.hash = ""; return u.href; })()`,
35
+ `(() => { const u = new U("https://a.com/x"); u.pathname = "new path"; return u.href; })()`,
36
+ `(() => { const u = new U("https://a.com/x?a=1"); u.searchParams.set("b", "two words"); return u.href; })()`,
37
+ `(() => { const u = new U("https://a.com/x?a=1&a=2"); u.searchParams.delete("a"); return u.href; })()`,
38
+ `(() => { const u = new U("https://a.com/x?b=2&a=1"); u.searchParams.sort(); return u.href; })()`,
39
+ `(() => { const u = new U("https://a.com/x?a=1"); const p = u.searchParams; u.search = "?z=9"; return p.get("z") + " " + p.get("a"); })()`,
40
+ `(() => { const u = new U("https://a.com/x"); u.searchParams.append("q", "a+b&c=d!'()~*"); return u.search; })()`,
41
+ `new U("https://a.com/x?a=1").searchParams.get("a")`,
42
+ `new U("http://u:p@h.com/").username + " " + new U("http://u:p@h.com/").password`,
43
+ `new U("http://u:p@h.com/").href`,
44
+ `new U("https://a.com/p").origin`,
45
+ `new U("http://[::1]:8080/").hostname + " " + new U("http://[::1]:8080/").port`,
46
+ `new U("http://[::1]:8080/").host`,
47
+ `U.canParse("nope") + " " + U.canParse("https://a.com") + " " + U.canParse("/x", "https://a.com")`,
48
+ `(() => { try { new U("nope"); return "no throw"; } catch (e) { return e instanceof TypeError; } })()`,
49
+ `(() => { try { new U("https://"); return "no throw"; } catch (e) { return e instanceof TypeError; } })()`,
50
+ `JSON.stringify({ u: new U("https://a.com") })`,
51
+ `new U("https://a.com").pathname`,
52
+ `new U("mailto:x@y.com").pathname + " " + new U("mailto:x@y.com").protocol + " " + new U("mailto:x@y.com").origin`,
53
+ `String(new U("https://a.com/a b"))`,
54
+ `new U("https://a.com/é?q=ü#ä").href`,
55
+ `new U("https://a.com/x?q=it's").search`,
56
+ `new U("HTTPS://A.COM/X").href`,
57
+ `new U("https://a.com?").href`,
58
+ `new U("https://a.com#").href`,
59
+ `new U("https://a.com/%2e/x").pathname`,
60
+ `(() => { const u = new U("https://a.com/x"); u.href = "http://b.com/y?z"; return u.host + " " + u.pathname + " " + u.search; })()`,
61
+ `new U("https://a.com/x?a=1#f").toString()`,
62
+ ];
63
+
64
+ const handler = `export default {fetch() {
65
+ const U = URL;
66
+ const cases = [
67
+ ${CASES.map((c) => ` () => (${c}),`).join("\n")}
68
+ ];
69
+ const out = [];
70
+ for (const run of cases) {
71
+ try { out.push(String(run())); } catch (e) { out.push("THROWS " + (e && e.message)); }
72
+ }
73
+ return new Response(JSON.stringify(out));
74
+ }};`;
75
+ const want = CASES.map((c) => String(new Function("U", "return " + c)(URL)));
76
+
77
+ function freePort(): number {
78
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
79
+ const port = listener.port;
80
+ listener.stop(true);
81
+ return port;
82
+ }
83
+
84
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-url-"));
85
+ let server: ReturnType<typeof Bun.spawn> | undefined;
86
+ try {
87
+ const porffor = await ensurePorffor();
88
+ const node = Bun.which("node");
89
+ if (!node) throw new Error("native streams test needs node on PATH");
90
+
91
+ const [core, broker] = await Promise.all([
92
+ readFile(preludePath, "utf8"),
93
+ readFile(transportPath("broker"), "utf8"),
94
+ ]);
95
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
96
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
97
+ const generated = join(workdir, "url.generated.js");
98
+ const binary = join(workdir, "url-native");
99
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
100
+ await rm(binary, { force: true });
101
+
102
+ const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
103
+ cwd: workdir,
104
+ env: process.env,
105
+ stdout: "pipe",
106
+ stderr: "pipe",
107
+ });
108
+ const [exitCode, stdout, stderr] = await Promise.all([
109
+ compile.exited,
110
+ new Response(compile.stdout).text(),
111
+ new Response(compile.stderr).text(),
112
+ ]);
113
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
114
+
115
+ const port = freePort();
116
+ server = Bun.spawn([binary], {
117
+ cwd: workdir,
118
+ env: { ...process.env, PORT: String(port) },
119
+ stdout: "ignore",
120
+ stderr: "inherit",
121
+ });
122
+ let ready = false;
123
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
124
+ try {
125
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
126
+ ready = true;
127
+ } catch {
128
+ if (server.exitCode !== null) break;
129
+ await Bun.sleep(100);
130
+ }
131
+ }
132
+ assert(ready, "compiled handler did not start");
133
+ // SAFETY: the handler above is the only producer of this body: a JSON array with one
134
+ // string per case; the comparison below reports any entry that differs.
135
+ const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
136
+ const diffs = CASES.flatMap((c, i) => (got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`]));
137
+ assert.deepEqual(diffs, [], `URL differs from WHATWG in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`);
138
+ console.log(`Native URL matches WHATWG URL in all ${CASES.length} cases: base URLs, setters, hash, ports, dot segments, encoding, searchParams`);
139
+ } finally {
140
+ if (server) {
141
+ server.kill(9);
142
+ await server.exited;
143
+ }
144
+ await rm(workdir, { recursive: true, force: true });
145
+ }
@@ -0,0 +1,92 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { ensurePorffor } from "../../toolchain/src/acquire";
6
+ import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
7
+
8
+ // ctx.waitUntil work runs after the response, as in workerd: the response does not wait
9
+ // for it, the work still finishes, and its 25 s cap leaves no timer behind once it is done.
10
+ const handler = `export default {async fetch(request, env, ctx) {
11
+ const path = new URL(request.url).pathname;
12
+ if (path === "/later") {
13
+ globalThis.__done = "pending";
14
+ ctx.waitUntil(new Promise((resolve) => setTimeout(() => { globalThis.__done = "done"; resolve(); }, 400)));
15
+ return new Response("scheduled");
16
+ }
17
+ if (path === "/state") return new Response(String(globalThis.__done));
18
+ return new Response("ok");
19
+ }};`;
20
+
21
+ function freePort(): number {
22
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
23
+ const port = listener.port;
24
+ listener.stop(true);
25
+ return port;
26
+ }
27
+
28
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-waituntil-"));
29
+ let server: ReturnType<typeof Bun.spawn> | undefined;
30
+ try {
31
+ const porffor = await ensurePorffor();
32
+ const node = Bun.which("node");
33
+ if (!node) throw new Error("native streams test needs node on PATH");
34
+
35
+ const [core, broker] = await Promise.all([
36
+ readFile(preludePath, "utf8"),
37
+ readFile(transportPath("broker"), "utf8"),
38
+ ]);
39
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
40
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
41
+ const generated = join(workdir, "waituntil.generated.js");
42
+ const binary = join(workdir, "waituntil-native");
43
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude, undefined, undefined, undefined, "2026-10-01"));
44
+ await rm(binary, { force: true });
45
+
46
+ const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
47
+ cwd: workdir,
48
+ env: process.env,
49
+ stdout: "pipe",
50
+ stderr: "pipe",
51
+ });
52
+ const [exitCode, stdout, stderr] = await Promise.all([
53
+ compile.exited,
54
+ new Response(compile.stdout).text(),
55
+ new Response(compile.stderr).text(),
56
+ ]);
57
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
58
+
59
+ const port = freePort();
60
+ server = Bun.spawn([binary], {
61
+ cwd: workdir,
62
+ env: { ...process.env, PORT: String(port) },
63
+ stdout: "ignore",
64
+ stderr: "inherit",
65
+ });
66
+ let ready = false;
67
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
68
+ try {
69
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
70
+ ready = true;
71
+ } catch {
72
+ if (server.exitCode !== null) break;
73
+ await Bun.sleep(100);
74
+ }
75
+ }
76
+ assert(ready, "compiled handler did not start");
77
+ const t0 = performance.now();
78
+ const first = await (await fetch(`http://127.0.0.1:${port}/later`)).text();
79
+ const elapsed = performance.now() - t0;
80
+ assert.equal(first, "scheduled");
81
+ assert(elapsed < 300, `the response waited ${Math.round(elapsed)} ms for its waitUntil work`);
82
+ assert.equal(await (await fetch(`http://127.0.0.1:${port}/state`)).text(), "pending");
83
+ await Bun.sleep(700);
84
+ assert.equal(await (await fetch(`http://127.0.0.1:${port}/state`)).text(), "done");
85
+ console.log(`Native waitUntil runs after the response (${Math.round(elapsed)} ms) and completes`);
86
+ } finally {
87
+ if (server) {
88
+ server.kill(9);
89
+ await server.exited;
90
+ }
91
+ await rm(workdir, { recursive: true, force: true });
92
+ }
@@ -0,0 +1,110 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { ensurePorffor } from "../../toolchain/src/acquire";
6
+ import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
7
+
8
+ // Web API behaviour the W2 conformance suite found wrong, compared with Bun: Headers
9
+ // (combined get, sorted iteration, getSetCookie, trimming, validation), TextDecoder (astral,
10
+ // replacement, fatal, BOM, stream), URLSearchParams init forms, atob/btoa checks, Unicode
11
+ // case mapping, queueMicrotask and IDN hosts.
12
+ const CASES = [
13
+ `(() => { const h = new Headers(); h.append("B", " 2 "); h.append("a", "1"); h.append("a", "x"); return [h.get("a"), [...h].map((p) => p.join("=")).join(";")].join("|"); })()`,
14
+ `(() => { const h = new Headers(); h.append("set-cookie", "a=1"); h.append("set-cookie", "b=2"); return [h.getSetCookie().join("+"), [...h].length].join("|"); })()`,
15
+ `(() => { try { new Headers({ "bad name": "1" }); return "no throw"; } catch (e) { return e.name; } })()`,
16
+ `(() => { try { new Headers({ a: "x\\ny" }); return "no throw"; } catch (e) { return e.name; } })()`,
17
+ `new TextDecoder().decode(new Uint8Array([0x61, 0xf0, 0x9f, 0x98, 0x80, 0xff, 0x62]))`,
18
+ `(() => { try { new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array([0xc3])); return "no throw"; } catch (e) { return e.name; } })()`,
19
+ `(() => { const d = new TextDecoder(); return d.decode(new Uint8Array([0xef, 0xbb, 0xbf, 0xe2, 0x82]), { stream: true }) + d.decode(new Uint8Array([0xac])); })()`,
20
+ `[new URLSearchParams({ a: "1", b: "x y" }).toString(), new URLSearchParams([["c", "3"]]).toString(), new URLSearchParams(new URLSearchParams("d=4")).toString()].join("|")`,
21
+ `(() => { try { atob("@@"); return "no throw"; } catch (e) { return e.name + ":" + atob(" aGVs bG8= "); } })()`,
22
+ `(() => { try { btoa("€"); return "no throw"; } catch (e) { return e.name; } })()`,
23
+ `["ÄÖÜ", "ΑΒΓ", "дж", "Ā", "Ωmega"].map((s) => s.toLowerCase() + s.toUpperCase()).join(",")`,
24
+ `new Promise((resolve) => { const o = []; setTimeout(() => o.push("t"), 0); queueMicrotask(() => o.push("m")); setTimeout(() => resolve(o.join()), 50); })`,
25
+ `new URL("http://münchen.test/x").host`,
26
+ `crypto.subtle.digest("SHA-1", new TextEncoder().encode("abc")).then((b) => Array.from(new Uint8Array(b), (x) => x.toString(16).padStart(2, "0")).join(""))`,
27
+ `crypto.subtle.importKey("raw", new TextEncoder().encode("key"), { name: "HMAC", hash: "SHA-1" }, true, ["sign"]).then(async (k) => [k.algorithm.hash.name, k.algorithm.length, Array.from(new Uint8Array(await crypto.subtle.sign("HMAC", k, new TextEncoder().encode("msg"))), (x) => x.toString(16).padStart(2, "0")).join(""), new Uint8Array(await crypto.subtle.exportKey("raw", k)).length].join())`,
28
+ `crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-512" }, true, ["sign"]).then(async (k) => [k.type, k.algorithm.length, (await crypto.subtle.exportKey("raw", k)).byteLength].join())`, `(async () => { const collect = async (rs) => { const r = rs.getReader(); const out = []; for (;;) { const { value, done } = await r.read(); if (done) return out; out.push(value); } }; const bytes = new ReadableStream({ start(c) { c.enqueue(new Uint8Array([0x61, 0xe2])); c.enqueue(new Uint8Array([0x82, 0xac, 0xf0, 0x9f])); c.enqueue(new Uint8Array([0x98, 0x80])); c.close(); } }); const text = new ReadableStream({ start(c) { c.enqueue("a\\uD83D"); c.enqueue("\\uDE00b"); c.close(); } }); return (await collect(bytes.pipeThrough(new TextDecoderStream()))).join("|") + ";" + (await collect(text.pipeThrough(new TextEncoderStream()))).map((u) => Array.from(u).join(",")).join("|"); })()`,
29
+ ];
30
+ const handler = `export default {async fetch() {
31
+ const cases = [
32
+ ${CASES.map((c) => ` () => (${c}),`).join("\n")}
33
+ ];
34
+ const out = [];
35
+ for (const run of cases) {
36
+ try { out.push(String(await run())); } catch (e) { out.push("THROWS " + (e && e.message)); }
37
+ }
38
+ return new Response(JSON.stringify(out));
39
+ }};`;
40
+ const want = await Promise.all(CASES.map(async (c) => { try { return String(await new Function("return " + c)()); } catch (e) { return "THROWS " + (e instanceof Error ? e.message : String(e)); } }));
41
+
42
+ function freePort(): number {
43
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
44
+ const port = listener.port;
45
+ listener.stop(true);
46
+ return port;
47
+ }
48
+
49
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-web-apis-"));
50
+ let server: ReturnType<typeof Bun.spawn> | undefined;
51
+ try {
52
+ const porffor = await ensurePorffor();
53
+ const node = Bun.which("node");
54
+ if (!node) throw new Error("native streams test needs node on PATH");
55
+
56
+ const [core, broker] = await Promise.all([
57
+ readFile(preludePath, "utf8"),
58
+ readFile(transportPath("broker"), "utf8"),
59
+ ]);
60
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
61
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
62
+ const generated = join(workdir, "web-apis.generated.js");
63
+ const binary = join(workdir, "web-apis-native");
64
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
65
+ await rm(binary, { force: true });
66
+
67
+ const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
68
+ cwd: workdir,
69
+ env: process.env,
70
+ stdout: "pipe",
71
+ stderr: "pipe",
72
+ });
73
+ const [exitCode, stdout, stderr] = await Promise.all([
74
+ compile.exited,
75
+ new Response(compile.stdout).text(),
76
+ new Response(compile.stderr).text(),
77
+ ]);
78
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
79
+
80
+ const port = freePort();
81
+ server = Bun.spawn([binary], {
82
+ cwd: workdir,
83
+ env: { ...process.env, PORT: String(port) },
84
+ stdout: "ignore",
85
+ stderr: "inherit",
86
+ });
87
+ let ready = false;
88
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
89
+ try {
90
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
91
+ ready = true;
92
+ } catch {
93
+ if (server.exitCode !== null) break;
94
+ await Bun.sleep(100);
95
+ }
96
+ }
97
+ assert(ready, "compiled handler did not start");
98
+ // SAFETY: the handler above is the only producer of this body: a JSON array with one
99
+ // string per case; the comparison below reports any entry that differs.
100
+ const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
101
+ const diffs = CASES.flatMap((c, i) => (got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`]));
102
+ assert.deepEqual(diffs, [], `Web API results differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`);
103
+ console.log(`Native Web APIs match Bun in all ${CASES.length} cases`);
104
+ } finally {
105
+ if (server) {
106
+ server.kill(9);
107
+ await server.exited;
108
+ }
109
+ await rm(workdir, { recursive: true, force: true });
110
+ }